Browse Source

Merge pull request #79 from BepInEx/overhaul-chainloder-cecil

Overhaul type loading and chainloading API
Geoffrey Horsington 4 years ago
parent
commit
2c0cde49b3

+ 1 - 1
.gitmodules

@@ -3,4 +3,4 @@
 	url = https://github.com/BepInEx/BepInEx.Harmony
 [submodule "submodules/MonoMod"]
 	path = submodules/MonoMod
-	url = https://github.com/0x0ade/MonoMod.git
+	url = https://github.com/MonoMod/MonoMod.git

+ 4 - 34
BepInEx.Patcher/BepInEx.Patcher.csproj

@@ -1,7 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <Import Project="..\packages\ILRepack.2.0.16\build\ILRepack.props" Condition="Exists('..\packages\ILRepack.2.0.16\build\ILRepack.props')" />
-  <Import Project="..\packages\ILRepack.MSBuild.Task.2.0.13\build\ILRepack.MSBuild.Task.props" Condition="Exists('..\packages\ILRepack.MSBuild.Task.2.0.13\build\ILRepack.MSBuild.Task.props')" />
   <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
   <PropertyGroup>
     <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
@@ -20,7 +19,7 @@
   <PropertyGroup>
     <StartupObject>BepInEx.Patcher.Program</StartupObject>
   </PropertyGroup>
-  <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Legacy|AnyCPU'">
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
     <DebugSymbols>true</DebugSymbols>
     <OutputPath>..\bin\patcher\</OutputPath>
     <DefineConstants>TRACE</DefineConstants>
@@ -30,29 +29,10 @@
     <ErrorReport>prompt</ErrorReport>
     <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
   </PropertyGroup>
-  <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'v2018|AnyCPU'">
-    <DebugSymbols>true</DebugSymbols>
-    <OutputPath>..\bin\patcher\</OutputPath>
-    <DefineConstants>TRACE;UNITY_2018</DefineConstants>
-    <Optimize>true</Optimize>
-    <DebugType>embedded</DebugType>
-    <PlatformTarget>AnyCPU</PlatformTarget>
-    <ErrorReport>prompt</ErrorReport>
-    <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
-  </PropertyGroup>
   <ItemGroup>
     <Reference Include="Mono.Cecil, Version=0.10.3.0, Culture=neutral, PublicKeyToken=50cebf1cceb9d05e, processorArchitecture=MSIL">
       <HintPath>..\packages\Mono.Cecil.0.10.3\lib\net35\Mono.Cecil.dll</HintPath>
     </Reference>
-    <Reference Include="Mono.Cecil.Mdb, Version=0.10.3.0, Culture=neutral, PublicKeyToken=50cebf1cceb9d05e, processorArchitecture=MSIL">
-      <HintPath>..\packages\Mono.Cecil.0.10.3\lib\net35\Mono.Cecil.Mdb.dll</HintPath>
-    </Reference>
-    <Reference Include="Mono.Cecil.Pdb, Version=0.10.3.0, Culture=neutral, PublicKeyToken=50cebf1cceb9d05e, processorArchitecture=MSIL">
-      <HintPath>..\packages\Mono.Cecil.0.10.3\lib\net35\Mono.Cecil.Pdb.dll</HintPath>
-    </Reference>
-    <Reference Include="Mono.Cecil.Rocks, Version=0.10.3.0, Culture=neutral, PublicKeyToken=50cebf1cceb9d05e, processorArchitecture=MSIL">
-      <HintPath>..\packages\Mono.Cecil.0.10.3\lib\net35\Mono.Cecil.Rocks.dll</HintPath>
-    </Reference>
     <Reference Include="System" />
     <Reference Include="System.Core" />
   </ItemGroup>
@@ -62,6 +42,7 @@
     <Compile Include="Properties\AssemblyInfo.cs" />
   </ItemGroup>
   <ItemGroup>
+    <None Include="ILRepack.targets" />
     <None Include="packages.config" />
   </ItemGroup>
   <ItemGroup />
@@ -73,19 +54,8 @@
     <PropertyGroup>
       <ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them.  For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
     </PropertyGroup>
-    <Error Condition="!Exists('..\packages\ILRepack.MSBuild.Task.2.0.13\build\ILRepack.MSBuild.Task.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\ILRepack.MSBuild.Task.2.0.13\build\ILRepack.MSBuild.Task.props'))" />
     <Error Condition="!Exists('..\packages\ILRepack.2.0.16\build\ILRepack.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\ILRepack.2.0.16\build\ILRepack.props'))" />
+    <Error Condition="!Exists('..\packages\ILRepack.Lib.MSBuild.Task.2.0.16.1\build\ILRepack.Lib.MSBuild.Task.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\ILRepack.Lib.MSBuild.Task.2.0.16.1\build\ILRepack.Lib.MSBuild.Task.targets'))" />
   </Target>
-  <!-- ILRepack -->
-  <Target Name="AfterBuild">
-    <Move SourceFiles="$(OutputPath)\$(AssemblyName).exe" DestinationFiles="$(OutputPath)\$(AssemblyName)1.exe" />
-    <ItemGroup>
-      <InputAssemblies Include="$(OutputPath)\$(AssemblyName)1.exe" />
-      <InputAssemblies Include="$(OutputPath)\Mono.Cecil.dll" />
-    </ItemGroup>
-    <ILRepack Parallel="true" Internalize="true" DebugInfo="true" PrimaryAssemblyFile="$(OutputPath)\$(AssemblyName)1.exe" InputAssemblies="@(InputAssemblies)" TargetKind="Exe" TargetPlatformVersion="v2" OutputFile="$(OutputPath)\$(AssemblyName).exe" />
-    <Delete Files="@(InputAssemblies)" />
-    <Delete Files="$(OutputPath)\BepInEx.Bootstrap.dll" />
-  </Target>
-  <!-- /ILRepack -->
+  <Import Project="..\packages\ILRepack.Lib.MSBuild.Task.2.0.16.1\build\ILRepack.Lib.MSBuild.Task.targets" Condition="Exists('..\packages\ILRepack.Lib.MSBuild.Task.2.0.16.1\build\ILRepack.Lib.MSBuild.Task.targets')" />
 </Project>

+ 23 - 0
BepInEx.Patcher/ILRepack.targets

@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <Target Name="ILRepacker" AfterTargets="Build" Condition="'$(Configuration)' == 'Release'">
+
+    <ItemGroup>
+      <InputAssemblies Include="$(OutputPath)\Mono.Cecil.dll" />
+    </ItemGroup>
+
+    <ILRepack
+      Parallel="true"
+      Internalize="true"
+      DebugInfo="true"
+      InputAssemblies="@(InputAssemblies)"
+      TargetKind="Exe"
+      TargetPlatformVersion="v2"
+      OutputFile="$(OutputPath)\$(AssemblyName).exe" />
+
+    <Delete Files="@(InputAssemblies)" />
+    <Delete Files="$(OutputPath)\BepInEx.Bootstrap.dll" />
+
+  </Target>
+</Project>

+ 10 - 11
BepInEx.Patcher/Program.cs

@@ -26,6 +26,14 @@ namespace BepInEx.Patcher
 			Console.ResetColor();
 		}
 
+		static string GetUnityEngineAssembly(string managedDir)
+		{
+			var path = Path.Combine(managedDir, "UnityEngine.CoreModule.dll");
+			if (File.Exists(path))
+				return path;
+			return Path.Combine(managedDir, "UnityEngine.dll");
+		}
+
 		static void Main(string[] args)
 		{
 			Console.WriteLine($"BepInEx Patcher v{Assembly.GetExecutingAssembly().GetName().Version}");
@@ -43,12 +51,7 @@ namespace BepInEx.Patcher
 
 				string managedDir = Environment.CurrentDirectory + $@"\{gameName}_Data\Managed";
 
-#if UNITY_2018
-				string unityOutputDLL = Path.GetFullPath($"{managedDir}\\UnityEngine.CoreModule.dll");
-#else
-				string unityOutputDLL = Path.GetFullPath($"{managedDir}\\UnityEngine.dll");
-#endif
-
+				string unityOutputDLL = GetUnityEngineAssembly(managedDir);
 
 				if (!Directory.Exists(managedDir) || !File.Exists(unityOutputDLL))
 					continue;
@@ -101,11 +104,7 @@ namespace BepInEx.Patcher
 					AssemblyResolver = defaultResolver
 				};
 
-#if UNITY_2018
-				string unityBackupDLL = Path.GetFullPath($"{managedDir}\\UnityEngine.CoreModule.dll.bak");
-#else
-				string unityBackupDLL = Path.GetFullPath($"{managedDir}\\UnityEngine.dll.bak");
-#endif
+				string unityBackupDLL = $"{GetUnityEngineAssembly(managedDir)}.bak";
 
 				//determine which assembly to use as a base
 				AssemblyDefinition unity = AssemblyDefinition.ReadAssembly(unityOutputDLL, rp);

+ 2 - 1
BepInEx.Patcher/packages.config

@@ -1,6 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
+
 <packages>
   <package id="ILRepack" version="2.0.16" targetFramework="net35" />
-  <package id="ILRepack.MSBuild.Task" version="2.0.13" targetFramework="net35" />
+  <package id="ILRepack.Lib.MSBuild.Task" version="2.0.16.1" targetFramework="net35" />
   <package id="Mono.Cecil" version="0.10.3" targetFramework="net35" />
 </packages>

+ 1 - 10
BepInEx.Preloader/BepInEx.Preloader.csproj

@@ -13,7 +13,7 @@
     <FileAlignment>512</FileAlignment>
     <Deterministic>true</Deterministic>
   </PropertyGroup>
-  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Legacy|AnyCPU' ">
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
     <DebugType>none</DebugType>
     <Optimize>true</Optimize>
     <OutputPath>..\bin\</OutputPath>
@@ -22,15 +22,6 @@
     <WarningLevel>4</WarningLevel>
     <DocumentationFile>..\bin\BepInEx.Preloader.xml</DocumentationFile>
   </PropertyGroup>
-  <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'v2018|AnyCPU'">
-    <OutputPath>..\bin\</OutputPath>
-    <DefineConstants>TRACE;UNITY_2018</DefineConstants>
-    <Optimize>true</Optimize>
-    <DebugType>none</DebugType>
-    <PlatformTarget>AnyCPU</PlatformTarget>
-    <ErrorReport>prompt</ErrorReport>
-    <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
-  </PropertyGroup>
   <ItemGroup>
     <Reference Include="Mono.Cecil, Version=0.10.3.0, Culture=neutral, PublicKeyToken=50cebf1cceb9d05e, processorArchitecture=MSIL">
       <HintPath>..\packages\Mono.Cecil.0.10.3\lib\net35\Mono.Cecil.dll</HintPath>

+ 47 - 23
BepInEx.Preloader/Entrypoint.cs

@@ -5,38 +5,18 @@ using System.Reflection;
 
 namespace BepInEx.Preloader
 {
-	internal static class Entrypoint
+	internal static class PreloaderRunner
 	{
-		/// <summary>
-		///     The main entrypoint of BepInEx, called from Doorstop.
-		/// </summary>
-		/// <param name="args">
-		///     The arguments passed in from Doorstop. First argument is the path of the currently executing
-		///     process.
-		/// </param>
-		public static void Main(string[] args)
+		public static void PreloaderMain(string[] args)
 		{
-			EnvVars.LoadVars();
-
 			string bepinPath = Path.GetDirectoryName(Path.GetDirectoryName(Path.GetFullPath(EnvVars.DOORSTOP_INVOKE_DLL_PATH)));
 
 			Paths.SetExecutablePath(args[0], bepinPath, EnvVars.DOORSTOP_MANAGED_FOLDER_DIR);
 			AppDomain.CurrentDomain.AssemblyResolve += LocalResolve;
-
 			Preloader.Run();
 		}
 
-		/// <summary>
-		///     A handler for <see cref="AppDomain.AssemblyResolve" /> to perform some special handling.
-		///     <para>
-		///         It attempts to check currently loaded assemblies (ignoring the version), and then checks the BepInEx/core path,
-		///         BepInEx/patchers path and the BepInEx folder, all in that order.
-		///     </para>
-		/// </summary>
-		/// <param name="sender"></param>
-		/// <param name="args"></param>
-		/// <returns></returns>
-		internal static Assembly LocalResolve(object sender, ResolveEventArgs args)
+		private static Assembly LocalResolve(object sender, ResolveEventArgs args)
 		{
 			var assemblyName = new AssemblyName(args.Name);
 
@@ -54,4 +34,48 @@ namespace BepInEx.Preloader
 			return null;
 		}
 	}
+
+	internal static class Entrypoint
+	{
+		private static string preloaderPath;
+
+		/// <summary>
+		///     The main entrypoint of BepInEx, called from Doorstop.
+		/// </summary>
+		/// <param name="args">
+		///     The arguments passed in from Doorstop. First argument is the path of the currently executing
+		///     process.
+		/// </param>
+		public static void Main(string[] args)
+		{
+			EnvVars.LoadVars();
+
+			// Get the path of this DLL via Doorstop env var because Assembly.Location mangles non-ASCII characters on some versions of Mono for unknown reasons
+			preloaderPath = Path.GetDirectoryName(Path.GetFullPath(EnvVars.DOORSTOP_INVOKE_DLL_PATH));
+
+			AppDomain.CurrentDomain.AssemblyResolve += ResolveCurrentDirectory;
+
+			// In some versions of Unity 4, Mono tries to resolve BepInEx.dll prematurely because of the call to Paths.SetExecutablePath
+			// To prevent that, we have to use reflection and a separate startup class so that we can install required assembly resolvers before the main code
+			typeof(Entrypoint).Assembly.GetType($"BepInEx.Preloader.{nameof(PreloaderRunner)}")
+							  ?.GetMethod(nameof(PreloaderRunner.PreloaderMain))
+							  ?.Invoke(null, new object[] { args });
+
+			AppDomain.CurrentDomain.AssemblyResolve -= ResolveCurrentDirectory;
+		}
+
+		private static Assembly ResolveCurrentDirectory(object sender, ResolveEventArgs args)
+		{
+			var name = new AssemblyName(args.Name);
+
+			try
+			{
+				return Assembly.LoadFile(Path.Combine(preloaderPath, $"{name.Name}.dll"));
+			}
+			catch (Exception)
+			{
+				return null;
+			}
+		}
+	}
 }

+ 0 - 1
BepInEx.Preloader/Logger/PreloaderLogWriter.cs

@@ -56,7 +56,6 @@ namespace BepInEx.Preloader
 
 		public void Dispose()
 		{
-
 			if (LoggerSource != null)
 			{
 				Console.SetOut(StandardOut);

+ 94 - 13
BepInEx.Preloader/Patching/AssemblyPatcher.cs

@@ -4,6 +4,7 @@ using System.Diagnostics;
 using System.IO;
 using System.Linq;
 using System.Reflection;
+using BepInEx.Bootstrap;
 using BepInEx.Configuration;
 using BepInEx.Logging;
 using BepInEx.Preloader.RuntimeFixes;
@@ -23,6 +24,8 @@ namespace BepInEx.Preloader.Patching
 	/// </summary>
 	internal static class AssemblyPatcher
 	{
+		private const BindingFlags ALL = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.IgnoreCase;
+
 		public static List<PatcherPlugin> PatcherPlugins { get; } = new List<PatcherPlugin>();
 
 		private static readonly string DumpedAssembliesPath = Path.Combine(Paths.BepInExRootPath, "DumpedAssemblies");
@@ -36,29 +39,108 @@ namespace BepInEx.Preloader.Patching
 			PatcherPlugins.Add(patcher);
 		}
 
+		private static T CreateDelegate<T>(MethodInfo method) where T : class => method != null ? Delegate.CreateDelegate(typeof(T), method) as T : null;
+
+		private static PatcherPlugin ToPatcherPlugin(TypeDefinition type)
+		{
+			if (type.IsInterface || type.IsAbstract && !type.IsSealed)
+				return null;
+
+			var targetDlls = type.Methods.FirstOrDefault(m => m.Name.Equals("get_TargetDLLs", StringComparison.InvariantCultureIgnoreCase) &&
+															  m.IsPublic &&
+															  m.IsStatic);
+
+			if (targetDlls == null ||
+				targetDlls.ReturnType.FullName != "System.Collections.Generic.IEnumerable`1<System.String>")
+				return null;
+
+			var patch = type.Methods.FirstOrDefault(m => m.Name.Equals("Patch") &&
+														 m.IsPublic &&
+														 m.IsStatic &&
+														 m.ReturnType.FullName == "System.Void" &&
+														 m.Parameters.Count == 1 &&
+														 (m.Parameters[0].ParameterType.FullName == "Mono.Cecil.AssemblyDefinition&" ||
+														  m.Parameters[0].ParameterType.FullName == "Mono.Cecil.AssemblyDefinition"));
+
+			if (patch == null)
+				return null;
+
+			return new PatcherPlugin
+			{
+				TypeName = type.FullName
+			};
+		}
+
 		/// <summary>
 		///     Adds all patchers from all managed assemblies specified in a directory.
 		/// </summary>
 		/// <param name="directory">Directory to search patcher DLLs from.</param>
 		/// <param name="patcherLocator">A function that locates assembly patchers in a given managed assembly.</param>
-		public static void AddPatchersFromDirectory(string directory,
-			Func<Assembly, List<PatcherPlugin>> patcherLocator)
+		public static void AddPatchersFromDirectory(string directory)
 		{
 			if (!Directory.Exists(directory))
 				return;
 
 			var sortedPatchers = new SortedDictionary<string, PatcherPlugin>();
 
-			foreach (string assemblyPath in Directory.GetFiles(directory, "*.dll", SearchOption.AllDirectories))
-				try
+			var patchers = TypeLoader.FindPluginTypes(directory, ToPatcherPlugin);
+
+			foreach (var keyValuePair in patchers)
+			{
+				var assemblyPath = keyValuePair.Key;
+				var patcherCollection = keyValuePair.Value;
+
+				var ass = Assembly.LoadFile(assemblyPath);
+
+				foreach (var patcherPlugin in patcherCollection)
 				{
-					var assembly = Assembly.LoadFrom(assemblyPath);
+					try
+					{
+						var type = ass.GetType(patcherPlugin.TypeName);
+
+						var methods = type.GetMethods(ALL);
+
+						patcherPlugin.Initializer = CreateDelegate<Action>(methods.FirstOrDefault(m => m.Name.Equals("Initialize", StringComparison.InvariantCultureIgnoreCase) &&
+																									   m.GetParameters().Length == 0 &&
+																									   m.ReturnType == typeof(void)));
+
+						patcherPlugin.Finalizer = CreateDelegate<Action>(methods.FirstOrDefault(m => m.Name.Equals("Finish", StringComparison.InvariantCultureIgnoreCase) &&
+																									 m.GetParameters().Length == 0 &&
+																									 m.ReturnType == typeof(void)));
+
+						patcherPlugin.TargetDLLs = CreateDelegate<Func<IEnumerable<string>>>(type.GetProperty("TargetDLLs", ALL).GetGetMethod());
+
+						var patcher = methods.FirstOrDefault(m => m.Name.Equals("Patch", StringComparison.CurrentCultureIgnoreCase) &&
+																  m.ReturnType == typeof(void) &&
+																  m.GetParameters().Length == 1 &&
+																  (m.GetParameters()[0].ParameterType == typeof(AssemblyDefinition) ||
+																   m.GetParameters()[0].ParameterType == typeof(AssemblyDefinition).MakeByRefType()));
+
+						patcherPlugin.Patcher = (ref AssemblyDefinition pAss) =>
+						{
+							//we do the array fuckery here to get the ref result out
+							object[] args = { pAss };
+
+							patcher.Invoke(null, args);
 
-					foreach (var patcher in patcherLocator(assembly))
-						sortedPatchers.Add(patcher.Name, patcher);
+							pAss = (AssemblyDefinition)args[0];
+						};
+
+						sortedPatchers.Add($"{ass.GetName().Name}/{type.FullName}", 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());
+					}
 				}
-				catch (BadImageFormatException) { } //unmanaged DLL
-				catch (ReflectionTypeLoadException) { } //invalid references
+
+				Logger.Log(patcherCollection.Any() ? LogLevel.Info : LogLevel.Debug,
+					$"Loaded {patcherCollection.Count} patcher methods from {ass.GetName().FullName}");
+			}
 
 			foreach (KeyValuePair<string, PatcherPlugin> patcher in sortedPatchers)
 				AddPatcher(patcher.Value);
@@ -123,10 +205,10 @@ namespace BepInEx.Preloader.Patching
 			// Then, perform the actual patching
 			var patchedAssemblies = new HashSet<string>();
 			foreach (var assemblyPatcher in PatcherPlugins)
-				foreach (string targetDll in assemblyPatcher.TargetDLLs)
+				foreach (string targetDll in assemblyPatcher.TargetDLLs())
 					if (assemblies.TryGetValue(targetDll, out var assembly))
 					{
-						Logger.LogInfo($"Patching [{assembly.Name.Name}] with [{assemblyPatcher.Name}]");
+						Logger.LogInfo($"Patching [{assembly.Name.Name}] with [{assemblyPatcher.TypeName}]");
 
 						assemblyPatcher.Patcher?.Invoke(ref assembly);
 						assemblies[targetDll] = assembly;
@@ -135,7 +217,6 @@ namespace BepInEx.Preloader.Patching
 
 
 			// Finally, load patched assemblies into memory
-
 			if (ConfigDumpAssemblies.Value || ConfigLoadDumpedAssemblies.Value)
 			{
 				if (!Directory.Exists(DumpedAssembliesPath))
@@ -153,7 +234,7 @@ namespace BepInEx.Preloader.Patching
 
 			if (ConfigBreakBeforeLoadAssemblies.Value)
 			{
-				Logger.LogInfo($"BepInEx is about load the following assemblies:\n{string.Join("\n", patchedAssemblies.ToArray())}");
+				Logger.LogInfo(data: $"BepInEx is about load the following assemblies:\n{String.Join("\n", patchedAssemblies.ToArray())}");
 				Logger.LogInfo($"The assemblies were dumped into {DumpedAssembliesPath}");
 				Logger.LogInfo("Load any assemblies into the debugger, set breakpoints and continue execution.");
 				Debugger.Break();

+ 16 - 4
BepInEx.Preloader/Patching/PatcherPlugin.cs

@@ -1,17 +1,19 @@
 using System;
 using System.Collections.Generic;
+using System.IO;
+using BepInEx.Bootstrap;
 
 namespace BepInEx.Preloader.Patching
 {
 	/// <summary>
 	///     A single assembly patcher.
 	/// </summary>
-	internal class PatcherPlugin
+	internal class PatcherPlugin : ICacheable
 	{
 		/// <summary>
 		///     Target assemblies to patch.
 		/// </summary>
-		public IEnumerable<string> TargetDLLs { get; set; } = null;
+		public Func<IEnumerable<string>> TargetDLLs { get; set; } = null;
 
 		/// <summary>
 		///     Initializer method that is run before any patching occurs.
@@ -29,8 +31,18 @@ namespace BepInEx.Preloader.Patching
 		public AssemblyPatcherDelegate Patcher { get; set; } = null;
 
 		/// <summary>
-		///     Name of the patcher.
+		///     Type name of the patcher.
 		/// </summary>
-		public string Name { get; set; } = string.Empty;
+		public string TypeName { get; set; } = string.Empty;
+
+		public void Save(BinaryWriter bw)
+		{
+			bw.Write(TypeName);
+		}
+
+		public void Load(BinaryReader br)
+		{
+			TypeName = br.ReadString();
+		}
 	}
 }

+ 39 - 122
BepInEx.Preloader/Preloader.cs

@@ -3,7 +3,6 @@ using System.Collections.Generic;
 using System.Diagnostics;
 using System.IO;
 using System.Linq;
-using System.Reflection;
 using System.Text;
 using BepInEx.Configuration;
 using BepInEx.Logging;
@@ -11,6 +10,7 @@ using BepInEx.Preloader.Patching;
 using BepInEx.Preloader.RuntimeFixes;
 using Mono.Cecil;
 using Mono.Cecil.Cil;
+using MonoMod.RuntimeDetour;
 using UnityInjector.ConsoleUtil;
 using MethodAttributes = Mono.Cecil.MethodAttributes;
 
@@ -26,24 +26,32 @@ namespace BepInEx.Preloader
 		/// </summary>
 		private static PreloaderConsoleListener PreloaderLog { get; set; }
 
+		public static bool IsPostUnity2017 { get; } = File.Exists(Path.Combine(Paths.ManagedPath, "UnityEngine.CoreModule.dll"));
+
 		public static void Run()
 		{
 			try
 			{
 				AllocateConsole();
 
-				if (ConfigApplyRuntimePatches.Value)
-					UnityPatches.Apply();
+				Utility.TryDo(() =>
+				{
+					if (ConfigShimHarmony.Value)
+						HarmonyDetourBridge.Init();
+				}, out var harmonyBridgeException);
+
+				Utility.TryDo(() =>
+				{
+					if (ConfigApplyRuntimePatches.Value)
+						UnityPatches.Apply();
+				}, out var runtimePatchException);
 
 				Logger.Sources.Add(TraceLogSource.CreateSource());
 
 				PreloaderLog = new PreloaderConsoleListener(ConfigPreloaderCOutLogging.Value);
-
 				Logger.Listeners.Add(PreloaderLog);
 
-				
-				string consoleTile = $"BepInEx {typeof(Paths).Assembly.GetName().Version} RC1 - {Process.GetCurrentProcess().ProcessName}";
-
+				string consoleTile = $"BepInEx {typeof(Paths).Assembly.GetName().Version} - {Process.GetCurrentProcess().ProcessName}";
 				ConsoleWindow.Title = consoleTile;
 				Logger.LogMessage(consoleTile);
 
@@ -53,35 +61,33 @@ namespace BepInEx.Preloader
 				if (attributes.Length > 0)
 				{
 					var attribute = (BuildInfoAttribute)attributes[0];
-
 					Logger.LogMessage(attribute.Info);
 				}
 
-#if UNITY_2018
-				Logger.LogMessage("Compiled in Unity v2018 mode");
-#else
-				Logger.LogMessage("Compiled in Legacy Unity mode");
-#endif
+				Logger.LogInfo($"Running under Unity v{FileVersionInfo.GetVersionInfo(Paths.ExecutablePath).FileVersion}");
+				Logger.LogInfo($"CLR runtime version: {Environment.Version}");
+				Logger.LogInfo($"Supports SRE: {Utility.CLRSupportsDynamicAssemblies}");
 
-				Logger.LogInfo($"Running under Unity v{Process.GetCurrentProcess().MainModule.FileVersionInfo.FileVersion}");
+				if (harmonyBridgeException != null)
+					Logger.LogWarning($"Failed to enable fix for Harmony for .NET Standard API. Error message: {harmonyBridgeException.Message}");
 
-				Logger.LogMessage("Preloader started");
+				if (runtimePatchException != null)
+					Logger.LogWarning($"Failed to apply runtime patches for Mono. See more info in the output log. Error message: {runtimePatchException.Message}");
 
+				Logger.LogMessage("Preloader started");
 
 				AssemblyPatcher.AddPatcher(new PatcherPlugin
-					{ TargetDLLs = new[] { ConfigEntrypointAssembly.Value },
-						Patcher = PatchEntrypoint,
-						Name = "BepInEx.Chainloader"
-					});
+				{
+					TargetDLLs = () => new[] { ConfigEntrypointAssembly.Value },
+					Patcher = PatchEntrypoint,
+					TypeName = "BepInEx.Chainloader"
+				});
 
-				AssemblyPatcher.AddPatchersFromDirectory(Paths.PatcherPluginPath, GetPatcherMethods);
+				AssemblyPatcher.AddPatchersFromDirectory(Paths.PatcherPluginPath);
 
 				Logger.LogInfo($"{AssemblyPatcher.PatcherPlugins.Count} patcher plugin(s) loaded");
 
-
 				AssemblyPatcher.PatchAndLoad(Paths.ManagedPath);
-
-
 				AssemblyPatcher.DisposePatchers();
 
 
@@ -123,97 +129,6 @@ namespace BepInEx.Preloader
 		}
 
 		/// <summary>
-		///     Scans the assembly for classes that use the patcher contract, and returns a list of valid patchers.
-		/// </summary>
-		/// <param name="assembly">The assembly to scan.</param>
-		/// <returns>A list of assembly patchers that were found in the assembly.</returns>
-		public static List<PatcherPlugin> GetPatcherMethods(Assembly assembly)
-		{
-			var patcherMethods = new List<PatcherPlugin>();
-			var flags = BindingFlags.Public | BindingFlags.Static | BindingFlags.IgnoreCase;
-
-			foreach (var type in assembly.GetExportedTypes())
-				try
-				{
-					if (type.IsInterface)
-						continue;
-
-					var targetsProperty = type.GetProperty("TargetDLLs",
-						flags,
-						null,
-						typeof(IEnumerable<string>),
-						Type.EmptyTypes,
-						null);
-
-					//first try get the ref patcher method
-					var patcher = type.GetMethod("Patch",
-						flags,
-						null,
-						CallingConventions.Any,
-						new[] { typeof(AssemblyDefinition).MakeByRefType() },
-						null);
-
-					if (patcher == null) //otherwise try getting the non-ref patcher method
-						patcher = type.GetMethod("Patch",
-							flags,
-							null,
-							CallingConventions.Any,
-							new[] { typeof(AssemblyDefinition) },
-							null);
-
-					if (targetsProperty == null || !targetsProperty.CanRead || patcher == null)
-						continue;
-
-					var assemblyPatcher = new PatcherPlugin();
-
-					assemblyPatcher.Name = $"{assembly.GetName().Name}/{type.FullName}";
-					assemblyPatcher.Patcher = (ref AssemblyDefinition ass) =>
-					{
-						//we do the array fuckery here to get the ref result out
-						object[] args = { ass };
-
-						patcher.Invoke(null, args);
-
-						ass = (AssemblyDefinition)args[0];
-					};
-
-					assemblyPatcher.TargetDLLs = (IEnumerable<string>)targetsProperty.GetValue(null, null);
-
-					var initMethod = type.GetMethod("Initialize",
-						flags,
-						null,
-						CallingConventions.Any,
-						Type.EmptyTypes,
-						null);
-
-					if (initMethod != null)
-						assemblyPatcher.Initializer = () => initMethod.Invoke(null, null);
-
-					var finalizeMethod = type.GetMethod("Finish",
-						flags,
-						null,
-						CallingConventions.Any,
-						Type.EmptyTypes,
-						null);
-
-					if (finalizeMethod != null)
-						assemblyPatcher.Finalizer = () => finalizeMethod.Invoke(null, null);
-
-					patcherMethods.Add(assemblyPatcher);
-				}
-				catch (Exception ex)
-				{
-					Logger.LogWarning($"Could not load patcher methods from {assembly.GetName().Name}");
-					Logger.LogWarning(ex);
-				}
-
-			Logger.Log(patcherMethods.Count > 0 ? LogLevel.Info : LogLevel.Debug,
-				$"Loaded {patcherMethods.Count} patcher methods from {assembly.GetName().Name}");
-
-			return patcherMethods;
-		}
-
-		/// <summary>
 		///     Inserts BepInEx's own chainloader entrypoint into UnityEngine.
 		/// </summary>
 		/// <param name="assembly">The assembly that will be attempted to be patched.</param>
@@ -281,9 +196,9 @@ namespace BepInEx.Preloader
 					il.InsertBefore(ins,
 						il.Create(OpCodes.Ldnull)); // gameExePath (always null, we initialize the Paths class in Entrypoint
 					il.InsertBefore(ins,
-						il.Create(OpCodes.Ldc_I4_0)); // startConsole (always false, we already load the console in Preloader)
+						il.Create(OpCodes.Ldc_I4_0)); //startConsole (always false, we already load the console in Preloader)
 					il.InsertBefore(ins,
-						il.Create(OpCodes.Call, initMethod)); // Chainloader.Initialize(string gameExePath, bool startConsole = true)
+						il.Create(OpCodes.Call, initMethod)); // Chainloader.Initialize(string gamePath, string managedPath = null, bool startConsole = true)
 					il.InsertBefore(ins,
 						il.Create(OpCodes.Call, startMethod));
 				}
@@ -323,12 +238,8 @@ namespace BepInEx.Preloader
 			"Preloader.Entrypoint",
 			"Assembly",
 			"The local filename of the assembly to target.",
-#if UNITY_2018
-			"UnityEngine.CoreModule.dll"
-#else
-			"UnityEngine.dll"
-#endif
-			);
+			IsPostUnity2017 ? "UnityEngine.CoreModule.dll" : "UnityEngine.dll"
+		);
 
 		private static readonly ConfigWrapper<string> ConfigEntrypointType = ConfigFile.CoreConfig.Wrap(
 			"Preloader.Entrypoint",
@@ -348,6 +259,12 @@ namespace BepInEx.Preloader
 			"Enables or disables runtime patches.\nThis should always be true, unless you cannot start the game due to a Harmony related issue (such as running .NET Standard runtime) or you know what you're doing.",
 			true);
 
+		private static readonly ConfigWrapper<bool> ConfigShimHarmony = ConfigFile.CoreConfig.Wrap(
+			"Preloader",
+			"ShimHarmonySupport",
+			"If enabled, basic Harmony functionality is patched to use MonoMod's RuntimeDetour instead.\nTry using this if Harmony does not work in a game.",
+			!Utility.CLRSupportsDynamicAssemblies);
+
 		private static readonly ConfigWrapper<bool> ConfigPreloaderCOutLogging = ConfigFile.CoreConfig.Wrap(
 			"Logging",
 			"PreloaderConsoleOutRedirection",

+ 0 - 13
BepInEx.Preloader/RuntimeFixes/UnityPatches.cs

@@ -39,18 +39,5 @@ namespace BepInEx.Preloader.RuntimeFixes
 			if (AssemblyLocations.TryGetValue(__instance.FullName, out string location))
 				__result = $"file://{location.Replace('\\', '/')}";
 		}
-
-#if UNITY_2018
-/*
- * DESC: Workaround for Trace class not working because of missing .config file
- * AFFECTS: Unity 2018+ (not .NET Standard / MonoBleedingEdge runtimes)
- */
-		[HarmonyPostfix, HarmonyPatch(typeof(AppDomain), nameof(AppDomain.SetupInformation), MethodType.Getter)]
-		public static void GetExeConfigName(AppDomainSetup __result)
-		{
-			__result.ApplicationBase = $"file://{Paths.GameRootPath}";
-			__result.ConfigurationFile = "app.config";
-		}
-#endif
 	}
 }

+ 1 - 0
BepInEx.Preloader/packages.config

@@ -1,4 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
+
 <packages>
   <package id="Mono.Cecil" version="0.10.3" targetFramework="net35" />
 </packages>

+ 19 - 36
BepInEx.sln

@@ -1,7 +1,7 @@
 
 Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio 15
-VisualStudioVersion = 15.0.27130.2027
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.28922.388
 MinimumVisualStudioVersion = 10.0.40219.1
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BepInEx", "BepInEx\BepInEx.csproj", "{4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}"
 EndProject
@@ -29,42 +29,25 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MonoMod.Utils", "submodules
 EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
-		Legacy|Any CPU = Legacy|Any CPU
-		v2018|Any CPU = v2018|Any CPU
+		Release|Any CPU = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(ProjectConfigurationPlatforms) = postSolution
-		{4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Legacy|Any CPU.ActiveCfg = Legacy|Any CPU
-		{4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Legacy|Any CPU.Build.0 = Legacy|Any CPU
-		{4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.v2018|Any CPU.ActiveCfg = v2018|Any CPU
-		{4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.v2018|Any CPU.Build.0 = v2018|Any CPU
-		{DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Legacy|Any CPU.ActiveCfg = Legacy|Any CPU
-		{DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Legacy|Any CPU.Build.0 = Legacy|Any CPU
-		{DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.v2018|Any CPU.ActiveCfg = v2018|Any CPU
-		{DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.v2018|Any CPU.Build.0 = v2018|Any CPU
-		{6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Legacy|Any CPU.ActiveCfg = Release|Any CPU
-		{6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Legacy|Any CPU.Build.0 = Release|Any CPU
-		{6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.v2018|Any CPU.ActiveCfg = Release|Any CPU
-		{6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.v2018|Any CPU.Build.0 = Release|Any CPU
-		{54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Legacy|Any CPU.ActiveCfg = Release|Any CPU
-		{54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Legacy|Any CPU.Build.0 = Release|Any CPU
-		{54161CFE-FF42-4DDE-B161-3A49545DB5CD}.v2018|Any CPU.ActiveCfg = Release|Any CPU
-		{54161CFE-FF42-4DDE-B161-3A49545DB5CD}.v2018|Any CPU.Build.0 = Release|Any CPU
-		{A15D6EE6-F954-415B-8605-8A8470CC87DC}.Legacy|Any CPU.ActiveCfg = Release|Any CPU
-		{A15D6EE6-F954-415B-8605-8A8470CC87DC}.Legacy|Any CPU.Build.0 = Release|Any CPU
-		{A15D6EE6-F954-415B-8605-8A8470CC87DC}.v2018|Any CPU.ActiveCfg = Release|Any CPU
-		{A15D6EE6-F954-415B-8605-8A8470CC87DC}.v2018|Any CPU.Build.0 = Release|Any CPU
-		{F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Legacy|Any CPU.ActiveCfg = Legacy|Any CPU
-		{F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Legacy|Any CPU.Build.0 = Legacy|Any CPU
-		{F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.v2018|Any CPU.ActiveCfg = v2018|Any CPU
-		{F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.v2018|Any CPU.Build.0 = v2018|Any CPU
-		{D0C584C0-81D7-486E-B70E-D7F9256E0909}.Legacy|Any CPU.ActiveCfg = Release|Any CPU
-		{D0C584C0-81D7-486E-B70E-D7F9256E0909}.Legacy|Any CPU.Build.0 = Release|Any CPU
-		{D0C584C0-81D7-486E-B70E-D7F9256E0909}.v2018|Any CPU.ActiveCfg = Release|Any CPU
-		{D0C584C0-81D7-486E-B70E-D7F9256E0909}.v2018|Any CPU.Build.0 = Release|Any CPU
-		{1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Legacy|Any CPU.ActiveCfg = Release|Any CPU
-		{1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Legacy|Any CPU.Build.0 = Release|Any CPU
-		{1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.v2018|Any CPU.ActiveCfg = Release|Any CPU
-		{1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.v2018|Any CPU.Build.0 = Release|Any CPU
+		{4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Release|Any CPU.Build.0 = Release|Any CPU
+		{DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Release|Any CPU.Build.0 = Release|Any CPU
+		{6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Release|Any CPU.Build.0 = Release|Any CPU
+		{54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Release|Any CPU.Build.0 = Release|Any CPU
+		{A15D6EE6-F954-415B-8605-8A8470CC87DC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{A15D6EE6-F954-415B-8605-8A8470CC87DC}.Release|Any CPU.Build.0 = Release|Any CPU
+		{F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Release|Any CPU.Build.0 = Release|Any CPU
+		{D0C584C0-81D7-486E-B70E-D7F9256E0909}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{D0C584C0-81D7-486E-B70E-D7F9256E0909}.Release|Any CPU.Build.0 = Release|Any CPU
+		{1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE

+ 2 - 9
BepInEx/BepInEx.csproj

@@ -13,7 +13,7 @@
     <AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
     <TargetFrameworkProfile />
   </PropertyGroup>
-  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Legacy|AnyCPU' ">
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
     <PlatformTarget>AnyCPU</PlatformTarget>
     <DebugType>none</DebugType>
     <Optimize>true</Optimize>
@@ -26,14 +26,6 @@
   <PropertyGroup>
     <StartupObject />
   </PropertyGroup>
-  <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'v2018|AnyCPU'">
-    <OutputPath>..\bin\</OutputPath>
-    <DefineConstants>TRACE;UNITY_2018</DefineConstants>
-    <Optimize>true</Optimize>
-    <PlatformTarget>AnyCPU</PlatformTarget>
-    <ErrorReport>prompt</ErrorReport>
-    <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
-  </PropertyGroup>
   <ItemGroup>
     <Reference Include="Mono.Cecil, Version=0.10.3.0, Culture=neutral, PublicKeyToken=50cebf1cceb9d05e, processorArchitecture=MSIL">
       <HintPath>..\packages\Mono.Cecil.0.10.3\lib\net35\Mono.Cecil.dll</HintPath>
@@ -45,6 +37,7 @@
     </Reference>
   </ItemGroup>
   <ItemGroup>
+    <Compile Include="Contract\PluginInfo.cs" />
     <Compile Include="Configuration\ConfigDefinition.cs" />
     <Compile Include="Configuration\ConfigFile.cs" />
     <Compile Include="Configuration\ConfigWrapper.cs" />

+ 118 - 94
BepInEx/Bootstrap/Chainloader.cs

@@ -8,6 +8,8 @@ using System.Linq;
 using System.Reflection;
 using System.Text;
 using System.Text.RegularExpressions;
+using BepInEx.Contract;
+using Mono.Cecil;
 using UnityEngine;
 using UnityInjector.ConsoleUtil;
 using Logger = BepInEx.Logging.Logger;
@@ -22,7 +24,9 @@ namespace BepInEx.Bootstrap
 		/// <summary>
 		/// The loaded and initialized list of plugins.
 		/// </summary>
-		public static List<BaseUnityPlugin> Plugins { get; private set; } = new List<BaseUnityPlugin>();
+		public static Dictionary<string, PluginInfo> PluginInfos { get; } = new Dictionary<string, PluginInfo>();
+
+		public static List<BaseUnityPlugin> Plugins { get; } = new List<BaseUnityPlugin>();
 
 		/// <summary>
 		/// The GameObject that all plugins are attached to as components.
@@ -34,15 +38,14 @@ namespace BepInEx.Bootstrap
 		private static bool _initialized = false;
 
 		/// <summary>
-        /// Initializes BepInEx to be able to start the chainloader.
-        /// </summary>
-        public static void Initialize(string gameExePath, bool startConsole = true)
+		/// Initializes BepInEx to be able to start the chainloader.
+		/// </summary>
+		public static void Initialize(string gameExePath, bool startConsole = true)
 		{
 			if (_initialized)
 				return;
 
 			// Set vitals
-
 			if (gameExePath != null)
 			{
 				// Checking for null allows a more advanced initialization workflow, where the Paths class has been initialized before calling Chainloader.Initialize
@@ -50,14 +53,12 @@ namespace BepInEx.Bootstrap
 				Paths.SetExecutablePath(gameExePath);
 			}
 
-			Paths.SetPluginPath(ConfigPluginsDirectory.Value);
-
-            // Start logging
-            if (ConsoleWindow.ConfigConsoleEnabled.Value && startConsole)
+			// Start logging
+			if (ConsoleWindow.ConfigConsoleEnabled.Value && startConsole)
 			{
 				ConsoleWindow.Attach();
 				Logger.Listeners.Add(new ConsoleLogListener());
-            }
+			}
 
 			// Fix for standard output getting overwritten by UnityLogger
 			if (ConsoleWindow.StandardOut != null)
@@ -69,7 +70,7 @@ namespace BepInEx.Bootstrap
 				Console.OutputEncoding = ConsoleEncoding.GetEncoding(encoding);
 			}
 
-            Logger.Listeners.Add(new UnityLogListener());
+			Logger.Listeners.Add(new UnityLogListener());
 			Logger.Listeners.Add(new DiskLogListener());
 
 			if (!TraceLogSource.IsListening)
@@ -84,7 +85,71 @@ namespace BepInEx.Bootstrap
 			_initialized = true;
 		}
 
-		private static Regex allowedGuidRegex { get; } = new Regex(@"^[a-zA-Z0-9\._]+$");
+		private static Regex allowedGuidRegex { get; } = new Regex(@"^[a-zA-Z0-9\._\-]+$");
+
+		public static PluginInfo ToPluginInfo(TypeDefinition type)
+		{
+			if (type.IsInterface || type.IsAbstract || !type.IsSubtypeOf(typeof(BaseUnityPlugin)))
+				return null;
+
+			var metadata = BepInPlugin.FromCecilType(type);
+
+			if (metadata == null)
+			{
+				Logger.LogWarning($"Skipping over type [{type.FullName}] as no metadata attribute is specified");
+				return null;
+			}
+
+			if (string.IsNullOrEmpty(metadata.GUID) || !allowedGuidRegex.IsMatch(metadata.GUID))
+			{
+				Logger.LogWarning($"Skipping type [{type.FullName}] because its GUID [{metadata.GUID}] is of an illegal format.");
+				return null;
+			}
+
+			if (metadata.Version == null)
+			{
+				Logger.LogWarning($"Skipping type [{type.FullName}] because its version is invalid.");
+				return null;
+			}
+
+			if (metadata.Name == null)
+			{
+				Logger.LogWarning($"Skipping type [{type.FullName}] because its name is null.");
+				return null;
+			}
+
+			//Perform a filter for currently running process
+			var filters = BepInProcess.FromCecilType(type);
+			bool invalidProcessName = filters.Count != 0 && filters.All(x => !string.Equals(x.ProcessName.Replace(".exe", ""), Paths.ProcessName, StringComparison.InvariantCultureIgnoreCase));
+
+			if (invalidProcessName)
+			{
+				Logger.LogWarning($"Skipping over plugin [{metadata.GUID}] due to process filter");
+				return null;
+			}
+
+			var dependencies = BepInDependency.FromCecilType(type);
+
+			return new PluginInfo
+			{
+				Metadata = metadata,
+				Processes = filters,
+				Dependencies = dependencies,
+				TypeName = type.FullName
+			};
+		}
+
+		private static readonly string CurrentAssemblyName = Assembly.GetExecutingAssembly().GetName().Name;
+
+		private static bool HasBepinPlugins(AssemblyDefinition ass)
+		{
+			if (ass.MainModule.AssemblyReferences.All(r => r.Name != CurrentAssemblyName))
+				return false;
+			if (ass.MainModule.GetTypeReferences().All(r => r.FullName != typeof(BaseUnityPlugin).FullName))
+				return false;
+
+			return true;
+		}
 
 		/// <summary>
 		/// The entrypoint for the BepInEx plugin system.
@@ -107,8 +172,7 @@ namespace BepInEx.Bootstrap
 			{
 				var productNameProp = typeof(Application).GetProperty("productName", BindingFlags.Public | BindingFlags.Static);
 				if (productNameProp != null)
-					ConsoleWindow.Title =
-						$"BepInEx {Assembly.GetExecutingAssembly().GetName().Version} - {productNameProp.GetValue(null, null)}";
+					ConsoleWindow.Title = $"BepInEx {Assembly.GetExecutingAssembly().GetName().Version} - {productNameProp.GetValue(null, null)}";
 
 				Logger.LogMessage("Chainloader started");
 
@@ -116,76 +180,34 @@ namespace BepInEx.Bootstrap
 
 				UnityEngine.Object.DontDestroyOnLoad(ManagerObject);
 
+				var pluginsToLoad = TypeLoader.FindPluginTypes(Paths.PluginPath, ToPluginInfo, HasBepinPlugins, "chainloader");
+				foreach (var keyValuePair in pluginsToLoad)
+					foreach (var pluginInfo in keyValuePair.Value)
+						pluginInfo.Location = keyValuePair.Key;
+				var pluginInfos = pluginsToLoad.SelectMany(p => p.Value).ToList();
+				var loadedAssemblies = new Dictionary<string, Assembly>();
 
-				string currentProcess = Process.GetCurrentProcess().ProcessName.ToLower();
-				
-				var globalPluginTypes = TypeLoader.LoadTypes<BaseUnityPlugin>(Paths.PluginPath).ToList();
+				Logger.LogInfo($"{pluginInfos.Count} plugins to load");
 
-				Dictionary<Type, BepInPlugin> selectedPluginTypes = new Dictionary<Type, BepInPlugin>(globalPluginTypes.Count);
+				var dependencyDict = new Dictionary<string, IEnumerable<string>>();
+				var pluginsByGUID = new Dictionary<string, PluginInfo>();
 
-				foreach (var pluginType in globalPluginTypes)
+				foreach (var pluginInfo in pluginInfos)
 				{
-					//Ensure metadata exists
-					var metadata = MetadataHelper.GetMetadata(pluginType);
-
-					if (metadata == null)
-					{
-						Logger.LogWarning($"Skipping type [{pluginType.FullName}] as no metadata attribute is specified");
-						continue;
-					}
-
-					if (string.IsNullOrEmpty(metadata.GUID) || !allowedGuidRegex.IsMatch(metadata.GUID))
-					{
-						Logger.LogWarning($"Skipping type [{pluginType.FullName}] because its GUID [{metadata.GUID}] is of an illegal format.");
-						continue;
-					}
-
-					if (selectedPluginTypes.Any(x => x.Value.GUID.Equals(metadata.GUID, StringComparison.OrdinalIgnoreCase)))
+					if (pluginInfo.Metadata.GUID == null)
 					{
-						Logger.LogWarning($"Skipping type [{pluginType.FullName}] because its GUID [{metadata.GUID}] is already used by another plugin.");
+						Logger.LogWarning($"Skipping [{pluginInfo.Metadata.Name}] because it does not have a valid GUID.");
 						continue;
 					}
 
-					if (metadata.Version == null)
+					if (dependencyDict.ContainsKey(pluginInfo.Metadata.GUID))
 					{
-						Logger.LogWarning($"Skipping type [{pluginType.FullName}] because its version is invalid.");
+						Logger.LogWarning($"Skipping [{pluginInfo.Metadata.Name}] because its GUID ({pluginInfo.Metadata.GUID}) is already used by another plugin.");
 						continue;
 					}
 
-					if (metadata.Name == null)
-					{
-						Logger.LogWarning($"Skipping type [{pluginType.FullName}] because its name is null.");
-						continue;
-					}
-
-					//Perform a filter for currently running process
-					var filters = MetadataHelper.GetAttributes<BepInProcess>(pluginType);
-
-					if (filters.Length != 0)
-					{
-						var result = filters.Any(x => x.ProcessName.ToLower().Replace(".exe", "") == currentProcess);
-
-						if (!result)
-						{
-							Logger.LogInfo($"Skipping over plugin [{metadata.GUID}] due to process filter");
-							continue;
-						}
-					}
-
-					selectedPluginTypes.Add(pluginType, metadata);
-				}
-
-				Logger.LogInfo($"{selectedPluginTypes.Count} / {globalPluginTypes.Count} plugins to load");
-
-				var dependencyDict = new Dictionary<string, IEnumerable<string>>();
-				var pluginsByGUID = new Dictionary<string, Type>();
-
-				foreach (var kv in selectedPluginTypes)
-				{
-					var dependencies = MetadataHelper.GetDependencies(kv.Key, selectedPluginTypes.Keys);
-
-					dependencyDict[kv.Value.GUID] = dependencies.Select(d => d.DependencyGUID);
-					pluginsByGUID[kv.Value.GUID] = kv.Key;
+					dependencyDict[pluginInfo.Metadata.GUID] = pluginInfo.Dependencies.Select(d => d.DependencyGUID);
+					pluginsByGUID[pluginInfo.Metadata.GUID] = pluginInfo;
 				}
 
 				var emptyDependencies = new string[0];
@@ -200,14 +222,12 @@ namespace BepInEx.Bootstrap
 				foreach (var pluginGUID in sortedPlugins)
 				{
 					// If the plugin is missing, don't process it
-					if (!pluginsByGUID.TryGetValue(pluginGUID, out var pluginType))
+					if (!pluginsByGUID.TryGetValue(pluginGUID, out var pluginInfo))
 						continue;
 
-					var metadata = MetadataHelper.GetMetadata(pluginType);
-					var dependencies = MetadataHelper.GetDependencies(pluginType, selectedPluginTypes.Keys);
 					var dependsOnInvalidPlugin = false;
 					var missingDependencies = new List<string>();
-					foreach (var dependency in dependencies)
+					foreach (var dependency in pluginInfo.Dependencies)
 					{
 						// If the depenency wasn't already processed, it's missing altogether
 						if (!processedPlugins.Contains(dependency.DependencyGUID))
@@ -230,14 +250,14 @@ namespace BepInEx.Bootstrap
 
 					if (dependsOnInvalidPlugin)
 					{
-						Logger.LogWarning($"Skipping [{metadata.Name}] because it has a dependency that was not loaded. See above errors for details.");
+						Logger.LogWarning($"Skipping [{pluginInfo.Metadata.Name}] because it has a dependency that was not loaded. See above errors for details.");
 						continue;
 					}
 
 					if (missingDependencies.Count != 0)
 					{
-						Logger.LogError($@"Missing the following dependencies for [{metadata.Name}]: {"\r\n"}{
-							string.Join("\r\n", missingDependencies.Select(s => $"- {s}").ToArray())
+						Logger.LogError($@"Missing the following dependencies for [{pluginInfo.Metadata.Name}]: {"\r\n"}{
+								string.Join("\r\n", missingDependencies.Select(s => $"- {s}").ToArray())
 							}{"\r\n"}Loading will be skipped; expect further errors and unstabilities.");
 
 						invalidPlugins.Add(pluginGUID);
@@ -246,14 +266,26 @@ namespace BepInEx.Bootstrap
 
 					try
 					{
-						Logger.LogInfo($"Loading [{metadata.Name} {metadata.Version}]");
-						Plugins.Add((BaseUnityPlugin)ManagerObject.AddComponent(pluginType));
+						Logger.LogInfo($"Loading [{pluginInfo.Metadata.Name} {pluginInfo.Metadata.Version}]");
+
+						if (!loadedAssemblies.TryGetValue(pluginInfo.Location, out var ass))
+							loadedAssemblies[pluginInfo.Location] = ass = Assembly.LoadFile(pluginInfo.Location);
+
+						PluginInfos[pluginGUID] = pluginInfo;
+						pluginInfo.Instance = (BaseUnityPlugin)ManagerObject.AddComponent(ass.GetType(pluginInfo.TypeName));
+
+						Plugins.Add(pluginInfo.Instance);
 					}
 					catch (Exception ex)
 					{
 						invalidPlugins.Add(pluginGUID);
-						Logger.LogError($"Error loading [{metadata.Name}] : {ex.Message}");
-						Logger.LogDebug(ex);
+						PluginInfos.Remove(pluginGUID);
+
+						Logger.LogError($"Error loading [{pluginInfo.Metadata.Name}] : {ex.Message}");
+						if (ex is ReflectionTypeLoadException re)
+							Logger.LogDebug(TypeLoader.TypeLoadExceptionToString(re));
+						else
+							Logger.LogDebug(ex);
 					}
 				}
 			}
@@ -272,17 +304,9 @@ namespace BepInEx.Bootstrap
 
 		#region Config
 
-		private static readonly ConfigWrapper<string> ConfigPluginsDirectory = ConfigFile.CoreConfig.Wrap(
-				"Paths",
-				"PluginsDirectory",
-				"The relative directory to the BepInEx folder where plugins are loaded.",
-				"plugins");
-
-		private static readonly ConfigWrapper<bool> ConfigUnityLogging = ConfigFile.CoreConfig.Wrap(
-				"Logging",
-				"UnityLogListening",
-				"Enables showing unity log messages in the BepInEx logging system.",
-				true);
+		private static readonly ConfigWrapper<string> ConfigPluginsDirectory = ConfigFile.CoreConfig.Wrap("Paths", "PluginsDirectory", "The relative directory to the BepInEx folder where plugins are loaded.", "plugins");
+
+		private static readonly ConfigWrapper<bool> ConfigUnityLogging = ConfigFile.CoreConfig.Wrap("Logging", "UnityLogListening", "Enables showing unity log messages in the BepInEx logging system.", true);
 
 		#endregion
 	}

+ 209 - 20
BepInEx/Bootstrap/TypeLoader.cs

@@ -1,56 +1,235 @@
 using System;
 using System.Collections.Generic;
 using System.IO;
+using System.Linq;
 using System.Reflection;
 using System.Text;
+using BepInEx.Configuration;
 using BepInEx.Logging;
+using Mono.Cecil;
 
 namespace BepInEx.Bootstrap
 {
 	/// <summary>
-	/// Provides methods for loading specified types from an assembly.
+	/// A cacheable metadata item. Can be used with <see cref="TypeLoader.LoadAssemblyCache{T}"/> and <see cref="TypeLoader.SaveAssemblyCache{T}"/> to cache plugin metadata.
+	/// </summary>
+	public interface ICacheable
+	{
+		/// <summary>
+		/// Serialize the object into a binary format.
+		/// </summary>
+		/// <param name="bw"></param>
+		void Save(BinaryWriter bw);
+
+		/// <summary>
+		/// Loads the object from binary format.
+		/// </summary>
+		/// <param name="br"></param>
+		void Load(BinaryReader br);
+	}
+
+	/// <summary>
+	/// A cached assembly.
+	/// </summary>
+	/// <typeparam name="T"></typeparam>
+	public class CachedAssembly<T> where T : ICacheable
+	{
+		/// <summary>
+		/// List of cached items inside the assembly.
+		/// </summary>
+		public List<T> CacheItems { get; set; }
+
+
+		/// <summary>
+		/// Timestamp of the assembly. Used to check the age of the cache.
+		/// </summary>
+		public long Timestamp { get; set; }
+	}
+
+	/// <summary>
+	///     Provides methods for loading specified types from an assembly.
 	/// </summary>
 	public static class TypeLoader
 	{
+		private static readonly DefaultAssemblyResolver resolver;
+		private static readonly ReaderParameters readerParameters;
+
+		static TypeLoader()
+		{
+			resolver = new DefaultAssemblyResolver();
+			readerParameters = new ReaderParameters { AssemblyResolver = resolver };
+
+			resolver.ResolveFailure += (sender, reference) =>
+			{
+				var name = new AssemblyName(reference.FullName);
+
+				if (Utility.TryResolveDllAssembly(name, Paths.BepInExAssemblyDirectory, readerParameters, out var assembly) ||
+					Utility.TryResolveDllAssembly(name, Paths.PluginPath, readerParameters, out assembly) ||
+					Utility.TryResolveDllAssembly(name, Paths.ManagedPath, readerParameters, out assembly))
+					return assembly;
+
+				return AssemblyResolve?.Invoke(sender, reference);
+			};
+		}
+
+		public static event AssemblyResolveEventHandler AssemblyResolve;
+
 		/// <summary>
-		/// Loads a list of types from a directory containing assemblies, that derive from a base type.
+		///     Looks up assemblies in the given directory and locates all types that can be loaded and collects their metadata.
 		/// </summary>
 		/// <typeparam name="T">The specific base type to search for.</typeparam>
 		/// <param name="directory">The directory to search for assemblies.</param>
-		/// <returns>Returns a list of found derivative types.</returns>
-		public static IEnumerable<Type> LoadTypes<T>(string directory)
+		/// <param name="typeSelector">A function to check if a type should be selected and to build the type metadata.</param>
+		/// <param name="assemblyFilter">A filter function to quickly determine if the assembly can be loaded.</param>
+		/// <param name="cacheName">The name of the cache to get cached types from.</param>
+		/// <returns>A list of all loadable type metadatas indexed by the full path to the assembly that contains the types.</returns>
+		public static Dictionary<string, List<T>> FindPluginTypes<T>(string directory, Func<TypeDefinition, T> typeSelector, Func<AssemblyDefinition, bool> assemblyFilter = null, string cacheName = null) where T : ICacheable, new()
 		{
-			List<Type> types = new List<Type>();
-			Type pluginType = typeof(T);
+			var result = new Dictionary<string, List<T>>();
+			Dictionary<string, CachedAssembly<T>> cache = null;
+
+			if (cacheName != null)
+				cache = LoadAssemblyCache<T>(cacheName);
 
 			foreach (string dll in Directory.GetFiles(Path.GetFullPath(directory), "*.dll", SearchOption.AllDirectories))
-			{
 				try
 				{
-					AssemblyName an = AssemblyName.GetAssemblyName(dll);
-					Assembly assembly = Assembly.Load(an);
+					if (cache != null && cache.TryGetValue(dll, out var cacheEntry))
+					{
+						long lastWrite = File.GetLastWriteTimeUtc(dll).Ticks;
+						if (lastWrite == cacheEntry.Timestamp)
+						{
+							result[dll] = cacheEntry.CacheItems;
+							continue;
+						}
+					}
+
+					var ass = AssemblyDefinition.ReadAssembly(dll, readerParameters);
+
+					if (!assemblyFilter?.Invoke(ass) ?? false)
+					{
+						ass.Dispose();
+						continue;
+					}
+
+					var matches = ass.MainModule.Types.Select(typeSelector).Where(t => t != null).ToList();
 
-					foreach (Type type in assembly.GetTypes())
+					if (matches.Count == 0)
 					{
-						if (!type.IsInterface && !type.IsAbstract && pluginType.IsAssignableFrom(type))
-							types.Add(type);
+						ass.Dispose();
+						continue;
 					}
+
+					result[dll] = matches;
+					ass.Dispose();
+				}
+				catch (Exception e)
+				{
+					Logger.LogError(e.ToString());
 				}
-				catch (BadImageFormatException) { } //unmanaged DLL
-				catch (ReflectionTypeLoadException ex)
+
+			if (cacheName != null)
+				SaveAssemblyCache(cacheName, result);
+
+			return result;
+		}
+
+		/// <summary>
+		///     Loads an index of type metadatas from a cache.
+		/// </summary>
+		/// <param name="cacheName">Name of the cache</param>
+		/// <typeparam name="T">Cacheable item</typeparam>
+		/// <returns>Cached type metadatas indexed by the path of the assembly that defines the type. If no cache is defined, return null.</returns>
+		public static Dictionary<string, CachedAssembly<T>> LoadAssemblyCache<T>(string cacheName) where T : ICacheable, new()
+		{
+			if (!EnableAssemblyCache.Value)
+				return null;
+
+			var result = new Dictionary<string, CachedAssembly<T>>();
+			try
+			{
+				string path = Path.Combine(Paths.CachePath, $"{cacheName}_typeloader.dat");
+				if (!File.Exists(path))
+					return null;
+
+				using (var br = new BinaryReader(File.OpenRead(path)))
 				{
-					Logger.LogError($"Could not load \"{Path.GetFileName(dll)}\" as a plugin!");
-					Logger.LogDebug(TypeLoadExceptionToString(ex));
+					int entriesCount = br.ReadInt32();
+
+					for (var i = 0; i < entriesCount; i++)
+					{
+						string entryIdentifier = br.ReadString();
+						long entryDate = br.ReadInt64();
+						int itemsCount = br.ReadInt32();
+						var items = new List<T>();
+
+						for (var j = 0; j < itemsCount; j++)
+						{
+							var entry = new T();
+							entry.Load(br);
+							items.Add(entry);
+						}
+
+						result[entryIdentifier] = new CachedAssembly<T> { Timestamp = entryDate, CacheItems = items };
+					}
 				}
 			}
+			catch (Exception e)
+			{
+				Logger.LogWarning($"Failed to load cache \"{cacheName}\"; skipping loading cache. Reason: {e.Message}.");
+			}
 
-			return types;
+			return result;
 		}
 
-		private static string TypeLoadExceptionToString(ReflectionTypeLoadException ex)
+		/// <summary>
+		///     Saves indexed type metadata into a cache.
+		/// </summary>
+		/// <param name="cacheName">Name of the cache</param>
+		/// <param name="entries">List of plugin metadatas indexed by the path to the assembly that contains the types</param>
+		/// <typeparam name="T">Cacheable item</typeparam>
+		public static void SaveAssemblyCache<T>(string cacheName, Dictionary<string, List<T>> entries) where T : ICacheable
 		{
-			StringBuilder sb = new StringBuilder();
-			foreach (Exception exSub in ex.LoaderExceptions)
+			if (!EnableAssemblyCache.Value)
+				return;
+
+			try
+			{
+				if (!Directory.Exists(Paths.CachePath))
+					Directory.CreateDirectory(Paths.CachePath);
+
+				string path = Path.Combine(Paths.CachePath, $"{cacheName}_typeloader.dat");
+
+				using (var bw = new BinaryWriter(File.OpenWrite(path)))
+				{
+					bw.Write(entries.Count);
+
+					foreach (var kv in entries)
+					{
+						bw.Write(kv.Key);
+						bw.Write(File.GetLastWriteTimeUtc(kv.Key).Ticks);
+						bw.Write(kv.Value.Count);
+
+						foreach (var item in kv.Value)
+							item.Save(bw);
+					}
+				}
+			}
+			catch (Exception e)
+			{
+				Logger.LogWarning($"Failed to save cache \"{cacheName}\"; skipping saving cache. Reason: {e.Message}.");
+			}
+		}
+
+		/// <summary>
+		///     Converts TypeLoadException to a readable string.
+		/// </summary>
+		/// <param name="ex">TypeLoadException</param>
+		/// <returns>Readable representation of the exception</returns>
+		public static string TypeLoadExceptionToString(ReflectionTypeLoadException ex)
+		{
+			var sb = new StringBuilder();
+			foreach (var exSub in ex.LoaderExceptions)
 			{
 				sb.AppendLine(exSub.Message);
 				if (exSub is FileNotFoundException exFileNotFound)
@@ -75,5 +254,15 @@ namespace BepInEx.Bootstrap
 
 			return sb.ToString();
 		}
+
+		#region Config
+
+		private static ConfigWrapper<bool> EnableAssemblyCache = ConfigFile.CoreConfig.Wrap(
+			"Caching", 
+			"EnableAssemblyCache", 
+			"Enable/disable assembly metadata cache\nEnabling this will speed up discovery of plugins and patchers by caching the metadata of all types BepInEx discovers.", 
+			true);
+
+		#endregion
 	}
 }

+ 1 - 1
BepInEx/Configuration/ConfigDefinition.cs

@@ -29,7 +29,7 @@
 				return false;
 
 			return string.Equals(Key, other.Key)
-			       && string.Equals(Section, other.Section);
+				   && string.Equals(Section, other.Section);
 		}
 
 		public override int GetHashCode()

+ 1 - 1
BepInEx/Configuration/TomlTypeConverter.cs

@@ -110,4 +110,4 @@ namespace BepInEx.Configuration
 			return (T)TypeConverters[typeof(T)].ConvertToObject(value);
 		}
 	}
-}
+}

+ 1 - 1
BepInEx/ConsoleUtil/ConsoleWindow.cs

@@ -25,7 +25,7 @@ namespace UnityInjector.ConsoleUtil
 			"If true, console is set to the Shift-JIS encoding, otherwise UTF-8 encoding.",
 			false);
 
-        public static bool IsAttached { get; private set; }
+		public static bool IsAttached { get; private set; }
 		private static IntPtr _cOut;
 		private static IntPtr _oOut;
 

+ 45 - 3
BepInEx/Contract/Attributes.cs

@@ -1,6 +1,10 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using System.Reflection;
+using BepInEx.Logging;
+using Mono.Cecil;
+using Mono.Collections.Generic;
 
 namespace BepInEx
 {
@@ -46,6 +50,16 @@ namespace BepInEx
 				this.Version = null;
 			}
 		}
+
+		internal static BepInPlugin FromCecilType(TypeDefinition td)
+		{
+			var attr = MetadataHelper.GetCustomAttributes<BepInPlugin>(td, false).FirstOrDefault();
+
+			if (attr == null)
+				return null;
+
+			return new BepInPlugin((string)attr.ConstructorArguments[0].Value, (string)attr.ConstructorArguments[1].Value, (string)attr.ConstructorArguments[2].Value);
+		}
 	}
 
 	/// <summary>
@@ -84,6 +98,12 @@ namespace BepInEx
 			this.DependencyGUID = DependencyGUID;
 			this.Flags = Flags;
 		}
+
+		internal static IEnumerable<BepInDependency> FromCecilType(TypeDefinition td)
+		{
+			var attrs = MetadataHelper.GetCustomAttributes<BepInDependency>(td, true);
+			return attrs.Select(customAttribute => new BepInDependency((string)customAttribute.ConstructorArguments[0].Value, (DependencyFlags)customAttribute.ConstructorArguments[1].Value)).ToList();
+		}
 	}
 
 	/// <summary>
@@ -102,6 +122,12 @@ namespace BepInEx
 		{
 			this.ProcessName = ProcessName;
 		}
+
+		internal static List<BepInProcess> FromCecilType(TypeDefinition td)
+		{
+			var attrs = MetadataHelper.GetCustomAttributes<BepInProcess>(td, true);
+			return attrs.Select(customAttribute => new BepInProcess((string)customAttribute.ConstructorArguments[0].Value)).ToList();
+		}
 	}
 
 	#endregion
@@ -113,6 +139,22 @@ namespace BepInEx
 	/// </summary>
 	public static class MetadataHelper
 	{
+		internal static IEnumerable<CustomAttribute> GetCustomAttributes<T>(TypeDefinition td, bool inherit) where T : Attribute
+		{
+			var result = new List<CustomAttribute>();
+			var type = typeof(T);
+			var currentType = td;
+
+			do
+			{
+				result.AddRange(currentType.CustomAttributes.Where(ca => ca.AttributeType.FullName == type.FullName));
+				currentType = currentType.BaseType?.Resolve();
+			} while (inherit && currentType?.FullName != "System.Object");
+
+
+			return result;
+		}
+
 		/// <summary>
 		/// Retrieves the BepInPlugin metadata from a plugin type.
 		/// </summary>
@@ -156,15 +198,15 @@ namespace BepInEx
 		public static IEnumerable<T> GetAttributes<T>(object plugin) where T : Attribute
 			=> GetAttributes<T>(plugin.GetType());
 
+
 		/// <summary>
 		/// Retrieves the dependencies of the specified plugin type.
 		/// </summary>
 		/// <param name="Plugin">The plugin type.</param>
-		/// <param name="AllPlugins">All currently loaded plugin types.</param>
 		/// <returns>A list of all plugin types that the specified plugin type depends upon.</returns>
-		public static IEnumerable<BepInDependency> GetDependencies(Type Plugin, IEnumerable<Type> AllPlugins)
+		public static IEnumerable<BepInDependency> GetDependencies(Type plugin)
 		{
-			return Plugin.GetCustomAttributes(typeof(BepInDependency), true).Cast<BepInDependency>();
+			return plugin.GetCustomAttributes(typeof(BepInDependency), true).Cast<BepInDependency>();
 		}
 	}
 

+ 19 - 1
BepInEx/Contract/BaseUnityPlugin.cs

@@ -1,4 +1,6 @@
-using BepInEx.Configuration;
+using BepInEx.Bootstrap;
+using BepInEx.Configuration;
+using BepInEx.Contract;
 using BepInEx.Logging;
 using UnityEngine;
 
@@ -13,10 +15,26 @@ namespace BepInEx
 
 		protected ConfigFile Config { get; }
 
+		protected PluginInfo Info { get; }
+
 		protected BaseUnityPlugin()
 		{
 			var metadata = MetadataHelper.GetMetadata(this);
 
+			if (Chainloader.PluginInfos.TryGetValue(metadata.GUID, out var info))
+				Info = info;
+			else
+			{
+				Info = new PluginInfo
+				{
+					Metadata = metadata,
+					Instance = this,
+					Dependencies = MetadataHelper.GetDependencies(GetType()),
+					Processes = MetadataHelper.GetAttributes<BepInProcess>(GetType()),
+					Location = GetType().Assembly.Location
+				};
+			}
+
 			Logger = Logging.Logger.CreateLogSource(metadata.Name);
 
 			Config = new ConfigFile(Utility.CombinePaths(Paths.ConfigPath, metadata.GUID + ".cfg"), false);

+ 63 - 0
BepInEx/Contract/PluginInfo.cs

@@ -0,0 +1,63 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using BepInEx.Bootstrap;
+
+namespace BepInEx.Contract
+{
+	public class PluginInfo : ICacheable
+	{
+		public BepInPlugin Metadata { get; internal set; }
+
+		public IEnumerable<BepInProcess> Processes { get; internal set; }
+
+		public IEnumerable<BepInDependency> Dependencies { get; internal set; }
+
+		public string Location { get; internal set; }
+
+		public BaseUnityPlugin Instance { get; internal set; }
+
+		internal string TypeName { get; set; }
+
+		public void Save(BinaryWriter bw)
+		{
+			bw.Write(TypeName);
+
+			bw.Write(Metadata.GUID);
+			bw.Write(Metadata.Name);
+			bw.Write(Metadata.Version.ToString());
+
+			var processList = Processes.ToList();
+			bw.Write(processList.Count);
+			foreach (var bepInProcess in processList)
+				bw.Write(bepInProcess.ProcessName);
+
+			var depList = Dependencies.ToList();
+			bw.Write(depList.Count);
+			foreach (var bepInDependency in depList)
+			{
+				bw.Write(bepInDependency.DependencyGUID);
+				bw.Write((int)bepInDependency.Flags);
+			}
+		}
+
+		public void Load(BinaryReader br)
+		{
+			TypeName = br.ReadString();
+
+			Metadata = new BepInPlugin(br.ReadString(), br.ReadString(), br.ReadString());
+
+			var processListCount = br.ReadInt32();
+			var processList = new List<BepInProcess>(processListCount);
+			for (int i = 0; i < processListCount; i++)
+				processList.Add(new BepInProcess(br.ReadString()));
+			Processes = processList;
+
+			var depCount = br.ReadInt32();
+			var depList = new List<BepInDependency>(depCount);
+			for (int i = 0; i < depCount; i++)
+				depList.Add(new BepInDependency(br.ReadString(), (BepInDependency.DependencyFlags) br.ReadInt32()));
+			Dependencies = depList;
+		}
+	}
+}

+ 1 - 1
BepInEx/Logging/ConsoleLogListener.cs

@@ -16,7 +16,7 @@ namespace BepInEx.Logging
 			if (eventArgs.Level.GetHighestLevel() > DisplayedLogLevel)
 				return;
 
-			string log = $"[{eventArgs.Level, -7}:{((ILogSource)sender).SourceName, 10}] {eventArgs.Data}\r\n";
+			string log = $"[{eventArgs.Level,-7}:{((ILogSource)sender).SourceName,10}] {eventArgs.Data}\r\n";
 
 			Kon.ForegroundColor = eventArgs.Level.GetConsoleColor();
 			Console.Write(log);

+ 2 - 5
BepInEx/Logging/DiskLogListener.cs

@@ -39,12 +39,9 @@ namespace BepInEx.Logging
 			}
 
 			LogWriter = TextWriter.Synchronized(new StreamWriter(fileStream, Encoding.UTF8));
-			
 
-			FlushTimer = new Timer(o =>
-			{
-				LogWriter?.Flush();
-			}, null, 2000, 2000);
+
+			FlushTimer = new Timer(o => { LogWriter?.Flush(); }, null, 2000, 2000);
 		}
 
 		public void LogEvent(object sender, LogEventArgs eventArgs)

+ 1 - 1
BepInEx/Logging/TraceLogSource.cs

@@ -76,7 +76,7 @@ namespace BepInEx.Logging
 					level = LogLevel.Debug;
 					break;
 			}
-			
+
 			LogSource.Log(level, $"{message}".Trim());
 		}
 	}

+ 1 - 1
BepInEx/Logging/UnityLogListener.cs

@@ -27,7 +27,7 @@ namespace BepInEx.Logging
 				WriteStringToUnityLog = (Action<string>)Delegate.CreateDelegate(typeof(Action<string>), methodInfo);
 				break;
 			}
-			
+
 			if (WriteStringToUnityLog == null)
 				Logger.LogError("Unable to start Unity log writer");
 		}

+ 1 - 0
BepInEx/Logging/UnityLogSource.cs

@@ -24,6 +24,7 @@ namespace BepInEx.Logging
 		}
 
 		private bool disposed = false;
+
 		public void Dispose()
 		{
 			if (!disposed)

+ 13 - 0
BepInEx/Paths.cs

@@ -21,6 +21,14 @@ namespace BepInEx
 			PatcherPluginPath = Path.Combine(BepInExRootPath, "patchers");
 			BepInExAssemblyDirectory = Path.Combine(BepInExRootPath, "core");
 			BepInExAssemblyPath = Path.Combine(BepInExAssemblyDirectory, $"{Assembly.GetExecutingAssembly().GetName().Name}.dll");
+			CachePath = Path.Combine(BepInExRootPath, "cache");
+		}
+
+		internal static void SetManagedPath(string managedPath)
+		{
+			if (managedPath == null)
+				return;
+			ManagedPath = managedPath;
 		}
 
 		internal static void SetPluginPath(string pluginPath)
@@ -69,6 +77,11 @@ namespace BepInEx
 		public static string BepInExConfigPath { get; private set; }
 
 		/// <summary>
+        ///		The path to temporary cache files.
+        /// </summary>
+		public static string CachePath { get; private set; }
+
+		/// <summary>
 		///     The path to the patcher plugin folder which resides in the BepInEx folder.
 		/// </summary>
 		public static string PatcherPluginPath { get; private set; }

+ 92 - 12
BepInEx/Utility.cs

@@ -3,6 +3,8 @@ using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 using System.Reflection;
+using System.Reflection.Emit;
+using Mono.Cecil;
 
 namespace BepInEx
 {
@@ -12,11 +14,50 @@ namespace BepInEx
 	public static class Utility
 	{
 		/// <summary>
-		/// Combines multiple paths together, as the specific method is not available in .NET 3.5.
+		/// Whether current Common Language Runtime supports dynamic method generation using <see cref="System.Reflection.Emit"/> namespace.
 		/// </summary>
-		/// <param name="parts">The multiple paths to combine together.</param>
-		/// <returns>A combined path.</returns>
-		public static string CombinePaths(params string[] parts) => parts.Aggregate(Path.Combine);
+		public static bool CLRSupportsDynamicAssemblies { get; }
+
+		static Utility()
+		{
+			try
+			{
+				var m = new DynamicMethod("SRE_Test", null, null);
+				CLRSupportsDynamicAssemblies = true;
+			}
+			catch (PlatformNotSupportedException)
+			{
+				CLRSupportsDynamicAssemblies = false;
+			}
+		}
+
+		/// <summary>
+		/// Try to perform an action.
+		/// </summary>
+		/// <param name="action">Action to perform.</param>
+		/// <param name="exception">Possible exception that gets returned.</param>
+		/// <returns>True, if action succeeded, false if an exception occured.</returns>
+		public static bool TryDo(Action action, out Exception exception)
+		{
+			exception = null;
+			try
+			{
+				action();
+				return true;
+			}
+			catch (Exception e)
+			{
+				exception = e;
+				return false;
+			}
+		}
+
+        /// <summary>
+        /// Combines multiple paths together, as the specific method is not available in .NET 3.5.
+        /// </summary>
+        /// <param name="parts">The multiple paths to combine together.</param>
+        /// <returns>A combined path.</returns>
+        public static string CombinePaths(params string[] parts) => parts.Aggregate(Path.Combine);
 
 		/// <summary>
 		/// Tries to parse a bool, with a default value if unable to parse.
@@ -26,7 +67,7 @@ namespace BepInEx
 		/// <returns>Boolean value of input if able to be parsed, otherwise default value.</returns>
 		public static bool SafeParseBool(string input, bool defaultValue = false)
 		{
-			return bool.TryParse(input, out bool result) ? result : defaultValue;
+			return Boolean.TryParse(input, out bool result) ? result : defaultValue;
 		}
 
 		/// <summary>
@@ -46,7 +87,7 @@ namespace BepInEx
 		/// <returns>True if the value parameter is null or empty, or if value consists exclusively of white-space characters.</returns>
 		public static bool IsNullOrWhiteSpace(this string self)
 		{
-			return self == null || self.All(char.IsWhiteSpace);
+			return self == null || self.All(Char.IsWhiteSpace);
 		}
 
 		public static IEnumerable<TNode> TopologicalSort<TNode>(IEnumerable<TNode> nodes, Func<TNode, IEnumerable<TNode>> dependencySelector)
@@ -61,9 +102,8 @@ namespace BepInEx
 				Stack<TNode> currentStack = new Stack<TNode>();
 				if (!Visit(input, currentStack))
 				{
-					throw new Exception("Cyclic Dependency:\r\n" + currentStack
-																   .Select(x => $" - {x}") //append dashes
-																   .Aggregate((a, b) => $"{a}\r\n{b}")); //add new lines inbetween
+					throw new Exception("Cyclic Dependency:\r\n" + currentStack.Select(x => $" - {x}") //append dashes
+																			   .Aggregate((a, b) => $"{a}\r\n{b}")); //add new lines inbetween
 				}
 			}
 
@@ -107,7 +147,7 @@ namespace BepInEx
 		/// <param name="directory">Directory to search the assembly from.</param>
 		/// <param name="assembly">The loaded assembly.</param>
 		/// <returns>True, if the assembly was found and loaded. Otherwise, false.</returns>
-		public static bool TryResolveDllAssembly(AssemblyName assemblyName, string directory, out Assembly assembly)
+		private static bool TryResolveDllAssembly<T>(AssemblyName assemblyName, string directory, Func<string, T> loader, out T assembly) where T : class
 		{
 			assembly = null;
 
@@ -124,7 +164,7 @@ namespace BepInEx
 
 				try
 				{
-					assembly = Assembly.LoadFile(path);
+					assembly = loader(path);
 				}
 				catch (Exception)
 				{
@@ -137,7 +177,47 @@ namespace BepInEx
 			return false;
 		}
 
-		public static bool TryOpenFileStream(string path, FileMode mode, out FileStream fileStream, FileAccess access = FileAccess.ReadWrite, FileShare share = FileShare.None)
+		public static bool IsSubtypeOf(this TypeDefinition self, Type td)
+		{
+			if (self.FullName == td.FullName)
+				return true;
+			return self.FullName != "System.Object" && (self.BaseType?.Resolve()?.IsSubtypeOf(td) ?? false);
+		}
+
+		/// <summary>
+		/// Try to resolve and load the given assembly DLL.
+		/// </summary>
+		/// <param name="assemblyName">Name of the assembly, of the type <see cref="AssemblyName" />.</param>
+		/// <param name="directory">Directory to search the assembly from.</param>
+		/// <param name="assembly">The loaded assembly.</param>
+		/// <returns>True, if the assembly was found and loaded. Otherwise, false.</returns>
+		public static bool TryResolveDllAssembly(AssemblyName assemblyName, string directory, out Assembly assembly)
+		{
+			return TryResolveDllAssembly(assemblyName, directory, Assembly.LoadFile, out assembly);
+		}
+
+		/// <summary>
+		/// Try to resolve and load the given assembly DLL.
+		/// </summary>
+		/// <param name="assemblyName">Name of the assembly, of the type <see cref="AssemblyName" />.</param>
+		/// <param name="directory">Directory to search the assembly from.</param>
+		/// <param name="assembly">The loaded assembly.</param>
+		/// <returns>True, if the assembly was found and loaded. Otherwise, false.</returns>
+		public static bool TryResolveDllAssembly(AssemblyName assemblyName, string directory, ReaderParameters readerParameters, out AssemblyDefinition assembly)
+		{
+			return TryResolveDllAssembly(assemblyName, directory, s => AssemblyDefinition.ReadAssembly(s, readerParameters), out assembly);
+		}
+
+		/// <summary>
+		/// Tries to create a file with the given name
+		/// </summary>
+		/// <param name="path">Path of the file to create</param>
+		/// <param name="mode">File open mode</param>
+		/// <param name="fileStream">Resulting filestream</param>
+		/// <param name="access">File access options</param>
+		/// <param name="share">File share options</param>
+		/// <returns></returns>
+		public static bool TryOpenFileStream(string path, FileMode mode, out FileStream fileStream, FileAccess access = FileAccess.ReadWrite, FileShare share = FileShare.Read)
 		{
 			try
 			{

+ 1 - 0
BepInEx/packages.config

@@ -1,4 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
+
 <packages>
   <package id="Mono.Cecil" version="0.10.3" targetFramework="net35" />
 </packages>

+ 2 - 2
README.md

@@ -21,9 +21,9 @@ Unity plugin framework
 **[User and developer guides](https://github.com/BepInEx/BepInEx/wiki)**
 
 ## Used libraries
-- [NeighTools/UnityDoorstop](https://github.com/NeighTools/UnityDoorstop) - 2.7.1.0 ([df7d636](https://github.com/NeighTools/UnityDoorstop/commit/df7d6366d8dc69f024c61cd31e6f690eb44ce57a))
+- [NeighTools/UnityDoorstop](https://github.com/NeighTools/UnityDoorstop) - 2.11.0.0 ([68a4e2d](https://github.com/NeighTools/UnityDoorstop/commit/68a4e2db1f09e5f5cc2f479d293b09764d09c80b))
 - [pardeike/Harmony](https://github.com/pardeike/Harmony) - pre-2.0 ([443f551](https://github.com/pardeike/Harmony/commit/443f551ec45ecf409755b5979a4466343197de03))
-- [0x0ade/MonoMod](https://github.com/0x0ade/MonoMod) - v19.05.01.01 ([934a8ae](https://github.com/0x0ade/MonoMod/commit/934a8ae921affac0093757d23c6f3ead34e996ac))
+- [0x0ade/MonoMod](https://github.com/0x0ade/MonoMod) - v19.08.02.03 ([a70072c](https://github.com/MonoMod/MonoMod/tree/a70072cdf759ac0cfa80991fcd2cca67d3eec130))
 - [jbevain/cecil](https://github.com/jbevain/cecil) - 0.10.3 ([fb289a7](https://github.com/jbevain/cecil/commit/fb289a7cd80ceb6af5c86e7c7ecce9bf1e98b8fe))
 
 ## Credits

+ 8 - 8
scripts/jenkins_master.groovy

@@ -46,12 +46,12 @@ Changes since ${latestTag}:
                 }
 
                 dir('Doorstop') {
-                    sh '''  tag="v2.7.1.0";
-                    version="2.7.1.0";
+                    sh '''  tag="v2.11.0.0";
+                    version="2.11.0.0";
                     wget https://github.com/NeighTools/UnityDoorstop/releases/download/$tag/Doorstop_x64_$version.zip;
                     wget https://github.com/NeighTools/UnityDoorstop/releases/download/$tag/Doorstop_x86_$version.zip;
-                    unzip -o Doorstop_x86_$version.zip winhttp.dll -d x86;
-                    unzip -o Doorstop_x64_$version.zip winhttp.dll -d x64;'''
+                    unzip -o Doorstop_x86_$version.zip version.dll -d x86;
+                    unzip -o Doorstop_x64_$version.zip version.dll -d x64;'''
                 }
             }
         }
@@ -120,7 +120,7 @@ Changes since ${latestTag}:
                     sh 'rm -rf BepInEx/core/patcher'
 
                     sh 'cp -f ../../../BepInEx/doorstop/doorstop_config.ini doorstop_config.ini'
-                    sh 'cp -f ../../../Doorstop/x86/winhttp.dll winhttp.dll'
+                    sh 'cp -f ../../../Doorstop/x86/version.dll version.dll'
                     
                     script {
                         if(params.IS_BE) {
@@ -134,7 +134,7 @@ Changes since ${latestTag}:
 
                     sh "zip -r9 BepInEx_Legacy_x86${commitPrefix}${versionNumber}.zip ./*"
                     
-                    sh 'cp -f ../../../Doorstop/x64/winhttp.dll winhttp.dll'
+                    sh 'cp -f ../../../Doorstop/x64/version.dll version.dll'
                     
                     sh 'unix2dos doorstop_config.ini'
                     
@@ -167,7 +167,7 @@ Changes since ${latestTag}:
                     sh 'rm -f BepInEx/core/UnityEngine.CoreModule.dll'
 
                     sh 'cp -f ../../../BepInEx/doorstop/doorstop_config.ini doorstop_config.ini'
-                    sh 'cp -f ../../../Doorstop/x86/winhttp.dll winhttp.dll'
+                    sh 'cp -f ../../../Doorstop/x86/version.dll version.dll'
                     
                     script {
                         if(params.IS_BE) {
@@ -181,7 +181,7 @@ Changes since ${latestTag}:
 
                     sh "zip -r9 BepInEx_v2018_x86${commitPrefix}${versionNumber}.zip ./*"
                     
-                    sh 'cp -f ../../../Doorstop/x64/winhttp.dll winhttp.dll'
+                    sh 'cp -f ../../../Doorstop/x64/version.dll version.dll'
                     
                     sh 'unix2dos doorstop_config.ini'
                     

+ 1 - 1
submodules/MonoMod

@@ -1 +1 @@
-Subproject commit 934a8ae921affac0093757d23c6f3ead34e996ac
+Subproject commit a70072cdf759ac0cfa80991fcd2cca67d3eec130