Browse Source

Merge pull request #29 from BepInEx/v40-framework

v4.0 framework
Bepis 6 years ago
parent
commit
632e34d336

+ 65 - 3
BepInEx.Common/Utility.cs

@@ -1,6 +1,9 @@
 using System;
+using System.Collections.Generic;
+using System.Diagnostics;
 using System.IO;
 using System.Linq;
+using System.Reflection;
 
 namespace BepInEx.Common
 {
@@ -10,14 +13,14 @@ namespace BepInEx.Common
     public static class Utility
     {
         /// <summary>
-        /// The directory that the Koikatsu .exe is being run from.
+        /// The directory that the game .exe is being run from.
         /// </summary>
-        public static string ExecutingDirectory => Path.GetDirectoryName(Environment.CommandLine);
+        public static string ExecutingDirectory { get; } = Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName);
 
         /// <summary>
         /// The path that the plugins folder is located.
         /// </summary>
-        public static string PluginsDirectory => Path.Combine(ExecutingDirectory, "BepInEx");
+        public static string PluginsDirectory { get; } = Path.Combine(ExecutingDirectory, "BepInEx");
 
         /// <summary>
         /// Combines multiple paths together, as the specfic method is not availble in .NET 3.5.
@@ -45,5 +48,64 @@ namespace BepInEx.Common
         {
             return self == null || self.Trim().Length == 0;
         }
+
+        public static IEnumerable<TNode> TopologicalSort<TNode>(IEnumerable<TNode> nodes, Func<TNode, IEnumerable<TNode>> dependencySelector)
+        {
+            List<TNode> sorted_list = new List<TNode>();
+
+            HashSet<TNode> visited = new HashSet<TNode>();
+            HashSet<TNode> sorted = new HashSet<TNode>();
+
+            foreach (TNode input in nodes)
+                Visit(input);
+
+            return sorted_list;
+
+            void Visit(TNode node)
+            {
+                if (visited.Contains(node))
+                {
+                    if (!sorted.Contains(node))
+                        throw new Exception("Cyclic Dependency");
+                }
+                else
+                {
+                    visited.Add(node);
+
+                    foreach (var dep in dependencySelector(node))
+                        Visit(dep);
+
+                    sorted.Add(node);
+                    sorted_list.Add(node);
+                }
+            }
+        }
+
+        /// <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)
+        {
+            assembly = null;
+            string path = Path.Combine(directory, $"{assemblyName.Name}.dll");
+
+            if (!File.Exists(path))
+                return false;
+
+            try
+            {
+                assembly = Assembly.LoadFile(path);
+            }
+            catch (Exception)
+            {
+                return false;
+            }
+
+            return true;
+        }
     }
 }

+ 4 - 2
BepInEx.sln

@@ -19,18 +19,20 @@ Global
 		BepInEx.Common\BepInEx.Common.projitems*{dc89f18b-235b-4c01-ab31-af40dce5c4c7}*SharedItemsImports = 4
 	EndGlobalSection
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Cecil 0.9.6_Release|Any CPU = Cecil 0.9.6_Release|Any CPU
 		Debug|Any CPU = Debug|Any CPU
 		Release|Any CPU = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Cecil 0.9.6_Release|Any CPU.ActiveCfg = Cecil 0.9.6_Release|Any CPU
+		{4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Cecil 0.9.6_Release|Any CPU.Build.0 = Cecil 0.9.6_Release|Any CPU
 		{4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Debug|Any CPU.ActiveCfg = Release|Any CPU
 		{4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Debug|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}.Cecil 0.9.6_Release|Any CPU.ActiveCfg = Release|Any CPU
 		{DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Debug|Any CPU.Build.0 = Debug|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
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE

+ 0 - 94
BepInEx/Attributes.cs

@@ -1,94 +0,0 @@
-using System;
-
-namespace BepInEx
-{
-    /// <summary>
-    /// This attribute denotes that a class is a plugin, and specifies the required metadata.
-    /// </summary>
-    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
-    public class BepInPlugin : Attribute
-    {
-        /// <summary>
-        /// The unique identifier of the plugin. Should not change between plugin versions.
-        /// </summary>
-        public string GUID { get; protected set; }
-
-        
-        /// <summary>
-        /// The user friendly name of the plugin. Is able to be changed between versions.
-        /// </summary>
-        public string Name { get; protected set; }
-
-        
-        /// <summary>
-        /// The specfic version of the plugin.
-        /// </summary>
-        public Version Version { get; protected set; }
-        
-        /// <param name="GUID">The unique identifier of the plugin. Should not change between plugin versions.</param>
-        /// <param name="Name">The user friendly name of the plugin. Is able to be changed between versions.</param>
-        /// <param name="Version">The specfic version of the plugin.</param>
-        public BepInPlugin(string GUID, string Name, string Version)
-        {
-            this.GUID = GUID;
-            this.Name = Name;
-            this.Version = new Version(Version);
-        }
-    }
-
-    /// <summary>
-    /// This attribute specifies any dependencies that this plugin has on other plugins.
-    /// </summary>
-    [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
-    public class BepInDependency : Attribute
-    {
-        public enum DependencyFlags
-        {
-            /// <summary>
-            /// The plugin has a hard dependency on the referenced plugin, and will not run without it.
-            /// </summary>
-            HardDependency = 1,
-
-            /// <summary>
-            /// This plugin has a soft dependency on the referenced plugin, and is able to run without it.
-            /// </summary>
-            SoftDependency = 2,
-        }
-
-        /// <summary>
-        /// The GUID of the referenced plugin.
-        /// </summary>
-        public string DependencyGUID { get; protected set; }
-
-        /// <summary>
-        /// The flags associated with this dependency definition.
-        /// </summary>
-        public DependencyFlags Flags { get; protected set; }
-        
-        /// <param name="DependencyGUID">The GUID of the referenced plugin.</param>
-        /// <param name="Flags">The flags associated with this dependency definition.</param>
-        public BepInDependency(string DependencyGUID, DependencyFlags Flags = DependencyFlags.HardDependency)
-        {
-            this.DependencyGUID = DependencyGUID;
-            this.Flags = Flags;
-        }
-    }
-
-    /// <summary>
-    /// This attribute specifies which processes this plugin should be run for. Not specifying this attribute will load the plugin under every process.
-    /// </summary>
-    [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
-    public class BepInProcess : Attribute
-    {
-        /// <summary>
-        /// The name of the process that this plugin will run under.
-        /// </summary>
-        public string ProcessName { get; protected set; }
-        
-        /// <param name="ProcessName">The name of the process that this plugin will run under.</param>
-        public BepInProcess(string ProcessName)
-        {
-            this.ProcessName = ProcessName;
-        }
-    }
-}

+ 31 - 8
BepInEx/BepInEx.csproj

@@ -18,17 +18,33 @@
     <DebugType>none</DebugType>
     <Optimize>true</Optimize>
     <OutputPath>..\bin\</OutputPath>
-    <DefineConstants>TRACE</DefineConstants>
+    <DefineConstants>TRACE;CECIL_10</DefineConstants>
     <ErrorReport>prompt</ErrorReport>
     <WarningLevel>4</WarningLevel>
+    <DocumentationFile>
+    </DocumentationFile>
   </PropertyGroup>
   <PropertyGroup>
     <StartupObject />
   </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Cecil 0.9.6_Release|AnyCPU'">
+    <OutputPath>..\bin\</OutputPath>
+    <DefineConstants>TRACE;CECIL_9</DefineConstants>
+    <Optimize>true</Optimize>
+    <PlatformTarget>AnyCPU</PlatformTarget>
+    <ErrorReport>prompt</ErrorReport>
+    <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+  </PropertyGroup>
   <ItemGroup>
     <Reference Include="0Harmony">
       <HintPath>..\lib\0Harmony.dll</HintPath>
     </Reference>
+    <Reference Include="Mono.Cecil" Condition="$(DefineConstants.Contains('CECIL_9'))">
+      <HintPath>..\lib\Cecil 9\Mono.Cecil.dll</HintPath>
+    </Reference>
+    <Reference Include="Mono.Cecil" Condition="$(DefineConstants.Contains('CECIL_10'))">
+      <HintPath>..\lib\Cecil 10\Mono.Cecil.dll</HintPath>
+    </Reference>
     <Reference Include="System" />
     <Reference Include="System.Xml" />
     <Reference Include="UnityEngine">
@@ -37,23 +53,30 @@
     </Reference>
   </ItemGroup>
   <ItemGroup>
-    <Compile Include="Attributes.cs" />
+    <Compile Include="Contract\Attributes.cs" />
+    <Compile Include="Bootstrap\AssemblyPatcher.cs" />
+    <Compile Include="Bootstrap\Preloader.cs" />
     <Compile Include="Config.cs" />
     <Compile Include="ConfigWrapper.cs" />
     <Compile Include="ConsoleUtil\ConsoleEncoding\ConsoleEncoding.Buffers.cs" />
     <Compile Include="ConsoleUtil\ConsoleEncoding\ConsoleEncoding.cs" />
     <Compile Include="ConsoleUtil\ConsoleEncoding\ConsoleEncoding.PInvoke.cs" />
-    <Compile Include="ConsoleUtil\ConsoleMirror.cs" />
     <Compile Include="ConsoleUtil\ConsoleWindow.cs" />
-    <Compile Include="ConsoleUtil\Extensions.cs" />
     <Compile Include="ConsoleUtil\Kon.cs" />
     <Compile Include="ConsoleUtil\SafeConsole.cs" />
-    <Compile Include="Chainloader.cs" />
-    <Compile Include="BaseUnityPlugin.cs" />
-    <Compile Include="Logger.cs" />
+    <Compile Include="Bootstrap\Chainloader.cs" />
+    <Compile Include="Contract\BaseUnityPlugin.cs" />
+    <Compile Include="Deprecated\BepInLogger.cs" />
+    <Compile Include="Logging\BaseLogger.cs" />
+    <Compile Include="Logging\Logger.cs" />
+    <Compile Include="Logging\LogLevel.cs" />
+    <Compile Include="Logging\PreloaderLogWriter.cs" />
+    <Compile Include="Logging\LoggerTraceListener.cs" />
+    <Compile Include="Logging\UnityLogWriter.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
-    <Compile Include="TypeLoader.cs" />
+    <Compile Include="Bootstrap\TypeLoader.cs" />
   </ItemGroup>
+  <ItemGroup />
   <Import Project="..\BepInEx.Common\BepInEx.Common.projitems" Label="Shared" />
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
 </Project>

+ 155 - 0
BepInEx/Bootstrap/AssemblyPatcher.cs

@@ -0,0 +1,155 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using BepInEx.Common;
+using Mono.Cecil;
+
+namespace BepInEx.Bootstrap
+{
+	/// <summary>
+	/// Delegate used in patching assemblies.
+	/// </summary>
+	/// <param name="assembly">The assembly that is being patched.</param>
+    public delegate void AssemblyPatcherDelegate(ref AssemblyDefinition assembly);
+
+	/// <summary>
+	/// Worker class which is used for loading and patching entire folders of assemblies, or alternatively patching and loading assemblies one at a time.
+	/// </summary>
+    public static class AssemblyPatcher
+    {
+		/// <summary>
+		/// Configuration value of whether assembly dumping is enabled or not.
+		/// </summary>
+        private static bool DumpingEnabled => bool.TryParse(Config.GetEntry("preloader-dumpassemblies", "false"), out bool result) ? result : false;
+
+        /// <summary>
+        /// Patches and loads an entire directory of assemblies.
+        /// </summary>
+        /// <param name="directory">The directory to load assemblies from.</param>
+        /// <param name="patcherMethodDictionary">The dictionary of patchers and their targeted assembly filenames which they are patching.</param>
+        /// <param name="initializers">List of initializers to run before any patching starts</param>
+        /// <param name="finalizers">List of finalizers to run before returning</param>
+        public static void PatchAll(string directory, IDictionary<AssemblyPatcherDelegate, IEnumerable<string>> patcherMethodDictionary, IEnumerable<Action> initializers = null, IEnumerable<Action> finalizers = null)
+        {
+			//run all initializers
+			if (initializers != null)
+				foreach (Action init in initializers)
+					init.Invoke();
+
+            //load all the requested assemblies
+            List<AssemblyDefinition> assemblies = new List<AssemblyDefinition>();
+            Dictionary<AssemblyDefinition, string> assemblyFilenames = new Dictionary<AssemblyDefinition, string>();
+
+            foreach (string assemblyPath in Directory.GetFiles(directory, "*.dll"))
+            {
+                var assembly = AssemblyDefinition.ReadAssembly(assemblyPath);
+                
+                //NOTE: this is special cased here because the dependency handling for System.dll is a bit wonky
+                //System has an assembly reference to itself, and it also has a reference to Mono.Security causing a circular dependency
+                //It's also generally dangerous to change system.dll since so many things rely on it, 
+                // and it's already loaded into the appdomain since this loader references it, so we might as well skip it
+                if (assembly.Name.Name == "System"
+                    || assembly.Name.Name == "mscorlib") //mscorlib is already loaded into the appdomain so it can't be patched
+                {
+#if CECIL_10
+                    assembly.Dispose();
+#endif
+                    continue;
+                }
+
+                assemblies.Add(assembly);
+                assemblyFilenames[assembly] = Path.GetFileName(assemblyPath);
+            }
+
+            //generate a dictionary of each assembly's dependencies
+            Dictionary<AssemblyDefinition, IList<AssemblyDefinition>> assemblyDependencyDict = new Dictionary<AssemblyDefinition, IList<AssemblyDefinition>>();
+            
+            foreach (AssemblyDefinition assembly in assemblies)
+            {
+                assemblyDependencyDict[assembly] = new List<AssemblyDefinition>();
+
+                foreach (var dependencyRef in assembly.MainModule.AssemblyReferences)
+                {
+                    var dependencyAssembly = assemblies.FirstOrDefault(x => x.FullName == dependencyRef.FullName);
+
+                    if (dependencyAssembly != null)
+                        assemblyDependencyDict[assembly].Add(dependencyAssembly);
+                }
+            }
+
+            //sort the assemblies so load the assemblies that are dependant upon first
+            AssemblyDefinition[] sortedAssemblies = Utility.TopologicalSort(assemblies, x => assemblyDependencyDict[x]).ToArray();
+
+	        List<string> sortedAssemblyFilenames = sortedAssemblies.Select(x => assemblyFilenames[x]).ToList();
+
+            //call the patchers on the assemblies
+	        foreach (var patcherMethod in patcherMethodDictionary)
+	        {
+		        foreach (string assemblyFilename in patcherMethod.Value)
+		        {
+			        int index = sortedAssemblyFilenames.FindIndex(x => x == assemblyFilename);
+
+			        if (index < 0)
+				        continue;
+
+					Patch(ref sortedAssemblies[index], patcherMethod.Key);
+		        }
+	        }
+
+
+			for (int i = 0; i < sortedAssemblies.Length; i++)
+			{
+                string filename = Path.GetFileName(assemblyFilenames[sortedAssemblies[i]]);
+
+                if (DumpingEnabled)
+                {
+                    using (MemoryStream mem = new MemoryStream())
+                    {
+                        string dirPath = Path.Combine(Preloader.PluginPath, "DumpedAssemblies");
+
+                        if (!Directory.Exists(dirPath))
+                            Directory.CreateDirectory(dirPath);
+                            
+	                    sortedAssemblies[i].Write(mem);
+                        File.WriteAllBytes(Path.Combine(dirPath, filename), mem.ToArray());
+                    }
+                }
+
+				Load(sortedAssemblies[i]);
+#if CECIL_10
+				sortedAssemblies[i].Dispose();
+#endif
+            }
+			
+	        //run all finalizers
+	        if (finalizers != null)
+		        foreach (Action finalizer in finalizers)
+			        finalizer.Invoke();
+        }
+
+		/// <summary>
+		/// Patches an individual assembly, without loading it.
+		/// </summary>
+		/// <param name="assembly">The assembly definition to apply the patch to.</param>
+		/// <param name="patcherMethod">The patcher to use to patch the assembly definition.</param>
+        public static void Patch(ref AssemblyDefinition assembly, AssemblyPatcherDelegate patcherMethod)
+        {
+	        patcherMethod.Invoke(ref assembly);
+        }
+
+		/// <summary>
+		/// Loads an individual assembly defintion into the CLR.
+		/// </summary>
+		/// <param name="assembly">The assembly to load.</param>
+	    public static void Load(AssemblyDefinition assembly)
+	    {
+		    using (MemoryStream assemblyStream = new MemoryStream())
+		    {
+			    assembly.Write(assemblyStream);
+			    Assembly.Load(assemblyStream.ToArray());
+		    }
+	    }
+    }
+}

+ 128 - 0
BepInEx/Bootstrap/Chainloader.cs

@@ -0,0 +1,128 @@
+using BepInEx.Common;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using BepInEx.Logging;
+using UnityEngine;
+using UnityInjector.ConsoleUtil;
+using UnityLogWriter = BepInEx.Logging.UnityLogWriter;
+
+namespace BepInEx.Bootstrap
+{
+	/// <summary>
+	/// The manager and loader for all plugins, and the entry point for BepInEx plugin system.
+	/// </summary>
+	public class Chainloader
+	{
+		/// <summary>
+		/// The loaded and initialized list of plugins.
+		/// </summary>
+		public static List<BaseUnityPlugin> Plugins { get; protected set; } = new List<BaseUnityPlugin>();
+
+		/// <summary>
+		/// The GameObject that all plugins are attached to as components.
+		/// </summary>
+		public static GameObject ManagerObject { get; protected set; } = new GameObject("BepInEx_Manager");
+
+
+		private static bool _loaded = false;
+
+		/// <summary>
+		/// The entry point for the BepInEx plugin system, called on the very first LoadScene() from UnityEngine.
+		/// </summary>
+		public static void Initialize()
+		{
+			if (_loaded)
+				return;
+
+		    if (!Directory.Exists(Utility.PluginsDirectory))
+		        Directory.CreateDirectory(Utility.PluginsDirectory);
+
+            Preloader.AllocateConsole();
+
+			try
+			{
+                UnityLogWriter unityLogWriter = new UnityLogWriter();
+
+			    if (Preloader.PreloaderLog != null)
+			        unityLogWriter.WriteToLog($"{Preloader.PreloaderLog}\r\n");
+
+                Logger.SetLogger(unityLogWriter);
+
+			    string consoleTile = $"BepInEx {Assembly.GetExecutingAssembly().GetName().Version} - {Application.productName}";
+			    ConsoleWindow.Title = consoleTile;
+                
+				Logger.Log(LogLevel.Message, "Chainloader started");
+
+				UnityEngine.Object.DontDestroyOnLoad(ManagerObject);
+
+
+			    string currentProcess = Process.GetCurrentProcess().ProcessName.ToLower();
+
+				var pluginTypes = TypeLoader.LoadTypes<BaseUnityPlugin>(Utility.PluginsDirectory)
+				    .Where(plugin =>
+				    {
+                        //Perform a filter for currently running process
+				        var filters = MetadataHelper.GetAttributes<BepInProcess>(plugin);
+
+				        if (!filters.Any())
+				            return true;
+
+				        return filters.Any(x => x.ProcessName.ToLower().Replace(".exe", "") == currentProcess);
+				    })
+				    .ToList();
+
+			    Logger.Log(LogLevel.Info, $"{pluginTypes.Count} plugins selected");
+
+				Dictionary<Type, IEnumerable<Type>> dependencyDict = new Dictionary<Type, IEnumerable<Type>>();
+
+				foreach (Type t in pluginTypes)
+				{
+					try
+					{
+						IEnumerable<Type> dependencies = MetadataHelper.GetDependencies(t, pluginTypes);
+
+						dependencyDict[t] = dependencies;
+					}
+					catch (MissingDependencyException)
+					{
+						var metadata = MetadataHelper.GetMetadata(t);
+
+					    Logger.Log(LogLevel.Info, $"Cannot load [{metadata.Name}] due to missing dependencies.");
+					}
+				}
+
+				pluginTypes = Utility.TopologicalSort(dependencyDict.Keys, x => dependencyDict[x]).ToList();
+
+				foreach (Type t in pluginTypes)
+				{
+					try
+					{
+						var metadata = MetadataHelper.GetMetadata(t);
+
+						var plugin = (BaseUnityPlugin) ManagerObject.AddComponent(t);
+
+						Plugins.Add(plugin);
+					    Logger.Log(LogLevel.Info, $"Loaded [{metadata.Name} {metadata.Version}]");
+					}
+					catch (Exception ex)
+					{
+					    Logger.Log(LogLevel.Info, $"Error loading [{t.Name}] : {ex.Message}");
+					}
+				}
+			}
+			catch (Exception ex)
+			{
+				UnityInjector.ConsoleUtil.ConsoleWindow.Attach();
+
+				Console.WriteLine("Error occurred starting the game");
+				Console.WriteLine(ex.ToString());
+			}
+
+			_loaded = true;
+		}
+	}
+}

+ 398 - 0
BepInEx/Bootstrap/Preloader.cs

@@ -0,0 +1,398 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using BepInEx.Common;
+using BepInEx.Logging;
+using Mono.Cecil;
+using Mono.Cecil.Cil;
+using UnityInjector.ConsoleUtil;
+using MethodAttributes = Mono.Cecil.MethodAttributes;
+
+namespace BepInEx.Bootstrap
+{
+	/// <summary>
+	/// The main entrypoint of BepInEx, and initializes all patchers and the chainloader.
+	/// </summary>
+    public static class Preloader
+    {
+        #region Path Properties
+
+        private static string executablePath;
+
+        /// <summary>
+        /// The path of the currently executing program BepInEx is encapsulated in.
+        /// </summary>
+        public static string ExecutablePath
+        {
+            get => executablePath;
+            set
+            {
+                executablePath = value;
+                GameRootPath = Path.GetDirectoryName(executablePath);
+                ManagedPath = Utility.CombinePaths(GameRootPath, $"{ProcessName}_Data", "Managed");
+                PluginPath = Utility.CombinePaths(GameRootPath, "BepInEx");
+                PatcherPluginPath = Utility.CombinePaths(GameRootPath, "BepInEx", "patchers");
+            }
+        }
+
+		/// <summary>
+		/// The path to the core BepInEx DLL.
+		/// </summary>
+        public static string BepInExAssemblyPath { get; } = typeof(Preloader).Assembly.CodeBase.Replace("file:///", "").Replace('/', '\\');
+
+		/// <summary>
+		/// The directory that the core BepInEx DLLs reside in.
+		/// </summary>
+	    public static string BepInExAssemblyDirectory { get; } = Path.GetDirectoryName(BepInExAssemblyPath);
+
+		/// <summary>
+		/// The name of the currently executing process.
+		/// </summary>
+        public static string ProcessName { get; } = Path.GetFileNameWithoutExtension(Process.GetCurrentProcess().ProcessName);
+
+		/// <summary>
+		/// The directory that the currently executing process resides in.
+		/// </summary>
+        public static string GameRootPath { get; private set; }
+
+		/// <summary>
+		/// The path to the Managed folder of the currently running Unity game.
+		/// </summary>
+        public static string ManagedPath { get; private set; }
+
+		/// <summary>
+		/// The path to the main BepInEx folder.
+		/// </summary>
+        public static string PluginPath { 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; }
+
+        #endregion
+
+		/// <summary>
+		/// The log writer that is specific to the preloader.
+		/// </summary>
+        public static PreloaderLogWriter PreloaderLog { get; private set; }
+
+		/// <summary>
+		/// The dictionary of currently loaded patchers. The key is the patcher delegate that will be used to patch, and the value is a list of filenames of assemblies that the patcher is targeting.
+		/// </summary>
+        public static Dictionary<AssemblyPatcherDelegate, IEnumerable<string>> PatcherDictionary { get; } = new Dictionary<AssemblyPatcherDelegate, IEnumerable<string>>();
+		
+		/// <summary>
+		/// The list of initializers that were loaded from the patcher contract.
+		/// </summary>
+	    public static List<Action> Initializers { get; } = new List<Action>();
+	    /// <summary>
+	    /// The list of finalizers that were loaded from the patcher contract.
+	    /// </summary>
+	    public static List<Action> Finalizers { get; } = new List<Action>();
+
+
+		/// <summary>
+		/// Adds the patcher to the patcher dictionary.
+		/// </summary>
+		/// <param name="dllNames">The list of DLL filenames to be patched.</param>
+		/// <param name="patcher">The method that will perform the patching.</param>
+        public static void AddPatcher(IEnumerable<string> dllNames, AssemblyPatcherDelegate patcher)
+        {
+	        PatcherDictionary[patcher] = dllNames;
+        }
+
+		/// <summary>
+		/// Safely retrieves a boolean value from the config. Returns false if not able to retrieve safely.
+		/// </summary>
+		/// <param name="key">The key to retrieve from the config.</param>
+		/// <param name="defaultValue">The default value to both return and set if the key does not exist in the config.</param>
+		/// <returns>The value of the key if found in the config, or the default value specified if not found, or false if it was unable to safely retrieve the value from the config.</returns>
+        private static bool SafeGetConfigBool(string key, string defaultValue)
+        {
+            try
+            {
+                string result = Config.GetEntry(key, defaultValue);
+
+                return bool.Parse(result);
+            }
+            catch
+            {
+                return false;
+            }
+        }
+
+		/// <summary>
+		/// Allocates a console window for use by BepInEx safely.
+		/// </summary>
+        internal static void AllocateConsole()
+        {
+            bool console = SafeGetConfigBool("console", "false");
+            bool shiftjis = SafeGetConfigBool("console-shiftjis", "false");
+
+            if (console)
+            {
+                try
+                {
+                    ConsoleWindow.Attach();
+
+                    uint encoding = (uint) Encoding.UTF8.CodePage;
+
+                    if (shiftjis)
+                        encoding = 932;
+
+                    ConsoleEncoding.ConsoleCodePage = encoding;
+                    Console.OutputEncoding = ConsoleEncoding.GetEncoding(encoding);
+                }
+                catch (Exception ex)
+                {
+                    Logger.Log(LogLevel.Error, "Failed to allocate console!");
+                    Logger.Log(LogLevel.Error, ex);
+                }
+            }
+        }
+
+		/// <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)
+        {
+            try
+            {
+                AppDomain.CurrentDomain.AssemblyResolve += LocalResolve;
+                ExecutablePath = args[0];
+
+                AllocateConsole();
+
+                PreloaderLog = new PreloaderLogWriter(SafeGetConfigBool("preloader-logconsole", "false"));
+                PreloaderLog.Enabled = true;
+
+                string consoleTile = $"BepInEx {Assembly.GetExecutingAssembly().GetName().Version} - {Process.GetCurrentProcess().ProcessName}";
+                ConsoleWindow.Title = consoleTile;
+                
+
+                Logger.SetLogger(PreloaderLog);
+                
+                PreloaderLog.WriteLine(consoleTile);
+                Logger.Log(LogLevel.Message, "Preloader started");
+
+
+                AddPatcher(new [] { "UnityEngine.dll" }, PatchEntrypoint);
+
+	            if (Directory.Exists(PatcherPluginPath))
+	            {
+		            SortedDictionary<string, KeyValuePair<AssemblyPatcherDelegate, IEnumerable<string>>> sortedPatchers = new SortedDictionary<string, KeyValuePair<AssemblyPatcherDelegate, IEnumerable<string>>>();
+
+					foreach (string assemblyPath in Directory.GetFiles(PatcherPluginPath, "*.dll"))
+                    {
+                        try
+                        {
+                            var assembly = Assembly.LoadFrom(assemblyPath);
+
+	                        foreach (var kv in GetPatcherMethods(assembly))
+		                        sortedPatchers.Add(assembly.GetName().Name, kv);
+                        }
+                        catch (BadImageFormatException) { } //unmanaged DLL
+                        catch (ReflectionTypeLoadException) { } //invalid references
+                    }
+
+		            foreach (var kv in sortedPatchers)
+			            AddPatcher(kv.Value.Value, kv.Value.Key);
+	            }
+
+                AssemblyPatcher.PatchAll(ManagedPath, PatcherDictionary, Initializers, Finalizers);
+            }
+            catch (Exception ex)
+            {
+                Logger.Log(LogLevel.Fatal, "Could not run preloader!");
+                Logger.Log(LogLevel.Fatal, ex);
+
+                PreloaderLog.Enabled = false;
+
+                try
+                {
+                    UnityInjector.ConsoleUtil.ConsoleWindow.Attach();
+                    Console.Write(PreloaderLog);
+                }
+                finally
+                {
+                    File.WriteAllText(Path.Combine(GameRootPath, $"preloader_{DateTime.Now:yyyyMMdd_HHmmss_fff}.log"),
+                        PreloaderLog.ToString());
+
+                    PreloaderLog.Dispose();
+                }
+            }
+        }
+
+		/// <summary>
+		/// Scans the assembly for classes that use the patcher contract, and returns a dictionary of the patch methods.
+		/// </summary>
+		/// <param name="assembly">The assembly to scan.</param>
+		/// <returns>A dictionary of delegates which will be used to patch the targeted assemblies.</returns>
+        internal static Dictionary<AssemblyPatcherDelegate, IEnumerable<string>> GetPatcherMethods(Assembly assembly)
+        {
+            var patcherMethods = new Dictionary<AssemblyPatcherDelegate, IEnumerable<string>>();
+
+            foreach (var type in assembly.GetExportedTypes())
+            {
+	            try
+	            {
+		            if (type.IsInterface)
+			            continue;
+
+		            PropertyInfo targetsProperty = type.GetProperty(
+			            "TargetDLLs",
+			            BindingFlags.Public | BindingFlags.Static | BindingFlags.IgnoreCase,
+			            null,
+			            typeof(IEnumerable<string>),
+			            Type.EmptyTypes,
+			            null);
+
+					//first try get the ref patcher method
+		            MethodInfo patcher = type.GetMethod(
+			            "Patch",
+			            BindingFlags.Public | BindingFlags.Static | BindingFlags.IgnoreCase,
+			            null,
+			            CallingConventions.Any,
+			            new[] {typeof(AssemblyDefinition).MakeByRefType()},
+			            null);
+
+		            if (patcher == null) //otherwise try getting the non-ref patcher method
+		            {
+			            patcher = type.GetMethod(
+				            "Patch",
+				            BindingFlags.Public | BindingFlags.Static | BindingFlags.IgnoreCase,
+				            null,
+				            CallingConventions.Any,
+				            new[] {typeof(AssemblyDefinition)},
+				            null);
+		            }
+
+		            if (targetsProperty == null || !targetsProperty.CanRead || patcher == null)
+                        continue;
+
+		            AssemblyPatcherDelegate patchDelegate = (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];
+		            };
+
+		            IEnumerable<string> targets = (IEnumerable<string>)targetsProperty.GetValue(null, null);
+
+		            patcherMethods[patchDelegate] = targets;
+
+
+
+		            MethodInfo initMethod = type.GetMethod(
+			            "Initialize",
+			            BindingFlags.Public | BindingFlags.Static | BindingFlags.IgnoreCase,
+			            null,
+			            CallingConventions.Any,
+			            Type.EmptyTypes,
+			            null);
+
+		            if (initMethod != null)
+		            {
+						Initializers.Add(() => initMethod.Invoke(null, null));
+		            }
+
+		            MethodInfo finalizeMethod = type.GetMethod(
+			            "Finish",
+			            BindingFlags.Public | BindingFlags.Static | BindingFlags.IgnoreCase,
+			            null,
+			            CallingConventions.Any,
+			            Type.EmptyTypes,
+			            null);
+
+		            if (finalizeMethod != null)
+		            {
+			            Finalizers.Add(() => finalizeMethod.Invoke(null, null));
+		            }
+	            }
+                catch (Exception ex)
+                {
+                    Logger.Log(LogLevel.Warning, $"Could not load patcher methods from {assembly.GetName().Name}");
+                    Logger.Log(LogLevel.Warning, $"{ex}");
+                }
+            }
+
+            Logger.Log(LogLevel.Info, $"Loaded {patcherMethods.Select(x => x.Key).Distinct().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>
+        internal static void PatchEntrypoint(ref AssemblyDefinition assembly)
+        {
+            if (assembly.Name.Name == "UnityEngine")
+            {
+#if CECIL_10
+                using (AssemblyDefinition injected = AssemblyDefinition.ReadAssembly(BepInExAssemblyPath))
+#elif CECIL_9
+                AssemblyDefinition injected = AssemblyDefinition.ReadAssembly(BepInExAssemblyPath);
+#endif
+                {
+                    var originalInjectMethod = injected.MainModule.Types.First(x => x.Name == "Chainloader")
+                        .Methods.First(x => x.Name == "Initialize");
+
+                    var injectMethod = assembly.MainModule.ImportReference(originalInjectMethod);
+
+                    var sceneManager = assembly.MainModule.Types.First(x => x.Name == "Application");
+
+                    var voidType = assembly.MainModule.ImportReference(typeof(void));
+                    var cctor = new MethodDefinition(".cctor",
+                        MethodAttributes.Static
+                        | MethodAttributes.Private
+                        | MethodAttributes.HideBySig
+                        | MethodAttributes.SpecialName
+                        | MethodAttributes.RTSpecialName,
+                        voidType);
+
+                    var ilp = cctor.Body.GetILProcessor();
+                    ilp.Append(ilp.Create(OpCodes.Call, injectMethod));
+                    ilp.Append(ilp.Create(OpCodes.Ret));
+
+                    sceneManager.Methods.Add(cctor);
+                }
+            }
+        }
+
+		/// <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)
+        {
+            AssemblyName assemblyName = new AssemblyName(args.Name);
+
+            var foundAssembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(x => x.GetName().Name == assemblyName.Name);
+
+            if (foundAssembly != null)
+                return foundAssembly;
+
+            if (Utility.TryResolveDllAssembly(assemblyName, BepInExAssemblyDirectory, out foundAssembly) ||
+                Utility.TryResolveDllAssembly(assemblyName, PatcherPluginPath, out foundAssembly) ||
+                Utility.TryResolveDllAssembly(assemblyName, PluginPath, out foundAssembly))
+                return foundAssembly;
+
+            return null;
+        }
+    }
+}

+ 48 - 0
BepInEx/Bootstrap/TypeLoader.cs

@@ -0,0 +1,48 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection;
+using BepInEx.Logging;
+
+namespace BepInEx.Bootstrap
+{
+	/// <summary>
+	/// Provides methods for loading specified types from an assembly.
+	/// </summary>
+    public static class TypeLoader
+    {
+        /// <summary>
+        /// Loads a list of types from a directory containing assemblies, that derive from a base type.
+        /// </summary>
+        /// <typeparam name="T">The specfiic 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)
+        {
+            List<Type> types = new List<Type>();
+            Type pluginType = typeof(T);
+
+            foreach (string dll in Directory.GetFiles(Path.GetFullPath(directory), "*.dll"))
+            {
+                try
+                {
+                    AssemblyName an = AssemblyName.GetAssemblyName(dll);
+                    Assembly assembly = Assembly.Load(an);
+
+                    foreach (Type type in assembly.GetTypes())
+                    {
+                        if (!type.IsInterface && !type.IsAbstract && type.BaseType == pluginType)
+                            types.Add(type);
+                    }
+                }
+                catch (BadImageFormatException) { } //unmanaged DLL
+                catch (ReflectionTypeLoadException)
+                {
+                    Logger.Log(LogLevel.Error, $"Could not load \"{Path.GetFileName(dll)}\" as a plugin!");
+                }
+            }
+
+            return types;
+        }
+    }
+}

+ 0 - 164
BepInEx/Chainloader.cs

@@ -1,164 +0,0 @@
-using BepInEx.Common;
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.IO;
-using System.Linq;
-using System.Reflection;
-using UnityEngine;
-
-namespace BepInEx
-{
-	/// <summary>
-	/// The manager and loader for all plugins, and the entry point for BepInEx.
-	/// </summary>
-	public class Chainloader
-	{
-		/// <summary>
-		/// The loaded and initialized list of plugins.
-		/// </summary>
-		public static List<BaseUnityPlugin> Plugins { get; protected set; } = new List<BaseUnityPlugin>();
-
-		/// <summary>
-		/// The GameObject that all plugins are attached to as components.
-		/// </summary>
-		public static GameObject ManagerObject { get; protected set; } = new GameObject("BepInEx_Manager");
-
-
-		static bool loaded = false;
-
-		/// <summary>
-		/// The entry point for BepInEx, called on the very first LoadScene() from UnityEngine.
-		/// </summary>
-		public static void Initialize()
-		{
-			if (loaded)
-				return;
-
-		    if (!Directory.Exists(Common.Utility.PluginsDirectory))
-		        Directory.CreateDirectory(Utility.PluginsDirectory);
-
-			if (bool.Parse(Config.GetEntry("console", "false")) || bool.Parse(Config.GetEntry("console-shiftjis", "false")))
-			{
-				try
-				{
-					UnityInjector.ConsoleUtil.ConsoleWindow.Attach();
-
-					if (bool.Parse(Config.GetEntry("console-shiftjis", "false")))
-						UnityInjector.ConsoleUtil.ConsoleEncoding.ConsoleCodePage = 932;
-				}
-				catch
-				{
-					BepInLogger.Log("Failed to allocate console!", true);
-				}
-			}
-
-			try
-			{
-				BepInLogger.Log($"BepInEx {Assembly.GetExecutingAssembly().GetName().Version}");
-				BepInLogger.Log("Chainloader started");
-
-				UnityEngine.Object.DontDestroyOnLoad(ManagerObject);
-
-
-			    string currentProcess = Process.GetCurrentProcess().ProcessName.ToLower();
-
-				var pluginTypes = TypeLoader.LoadTypes<BaseUnityPlugin>(Utility.PluginsDirectory)
-				    .Where(plugin =>
-				    {
-                        //Perform a filter for currently running process
-				        var filters = TypeLoader.GetAttributes<BepInProcess>(plugin);
-
-				        if (!filters.Any())
-				            return true;
-
-				        return filters.Any(x => x.ProcessName.ToLower().Replace(".exe", "") == currentProcess);
-				    })
-				    .ToList();
-
-				BepInLogger.Log($"{pluginTypes.Count} plugins selected");
-
-				Dictionary<Type, IEnumerable<Type>> dependencyDict = new Dictionary<Type, IEnumerable<Type>>();
-
-				foreach (Type t in pluginTypes)
-				{
-					try
-					{
-						IEnumerable<Type> dependencies = TypeLoader.GetDependencies(t, pluginTypes);
-
-						dependencyDict[t] = dependencies;
-					}
-					catch (MissingDependencyException)
-					{
-						var metadata = TypeLoader.GetMetadata(t);
-
-						BepInLogger.Log($"Cannot load [{metadata.Name}] due to missing dependencies.");
-					}
-				}
-
-				pluginTypes = TopologicalSort(dependencyDict.Keys, x => dependencyDict[x]).ToList();
-
-				foreach (Type t in pluginTypes)
-				{
-					try
-					{
-						var metadata = TypeLoader.GetMetadata(t);
-
-						var plugin = (BaseUnityPlugin) ManagerObject.AddComponent(t);
-
-						Plugins.Add(plugin);
-						BepInLogger.Log($"Loaded [{metadata.Name} {metadata.Version}]");
-					}
-					catch (Exception ex)
-					{
-						BepInLogger.Log($"Error loading [{t.Name}] : {ex.Message}");
-					}
-				}
-			}
-			catch (Exception ex)
-			{
-				UnityInjector.ConsoleUtil.ConsoleWindow.Attach();
-
-				Console.WriteLine("Error occurred starting the game");
-				Console.WriteLine(ex.ToString());
-			}
-
-			loaded = true;
-		}
-
-		protected static IEnumerable<TNode> TopologicalSort<TNode>(
-			IEnumerable<TNode> nodes,
-			Func<TNode, IEnumerable<TNode>> dependencySelector)
-		{
-
-			List<TNode> sorted_list = new List<TNode>();
-
-			HashSet<TNode> visited = new HashSet<TNode>();
-			HashSet<TNode> sorted = new HashSet<TNode>();
-
-			foreach (TNode input in nodes)
-				Visit(input);
-
-			return sorted_list;
-
-			void Visit(TNode node)
-			{
-				if (visited.Contains(node))
-				{
-					if (!sorted.Contains(node))
-						throw new Exception("Cyclic Dependency");
-				}
-				else
-				{
-					visited.Add(node);
-
-					foreach (var dep in dependencySelector(node))
-						Visit(dep);
-
-					sorted.Add(node);
-					sorted_list.Add(node);
-				}
-			}
-		}
-	}
-}

+ 23 - 14
BepInEx/Config.cs

@@ -15,7 +15,7 @@ namespace BepInEx
 
         private static string configPath => Path.Combine(Common.Utility.PluginsDirectory, "config.ini");
 
-        private static Regex sanitizeKeyRegex = new Regex("[^a-zA-Z0-9]+");
+        private static Regex sanitizeKeyRegex = new Regex(@"[^a-zA-Z0-9\-\.]+");
 
         private static void RaiseConfigReloaded()
         {
@@ -24,6 +24,9 @@ namespace BepInEx
                 handler.Invoke();
         }
 
+		/// <summary>
+		/// An event that is fired every time the config is reloaded.
+		/// </summary>
         public static event Action ConfigReloaded;
 
         /// <summary>
@@ -42,14 +45,15 @@ namespace BepInEx
                 SaveConfig();
             }
         }
-        
-        /// <summary>
-        /// Returns the value of the key if found, otherwise returns the default value.
-        /// </summary>
-        /// <param name="key">The key to search for.</param>
-        /// <param name="defaultValue">The default value to return if the key is not found.</param>
-        /// <returns>The value of the key.</returns>
-        public static string GetEntry(string key, string defaultValue = "", string section = "")
+
+	    /// <summary>
+	    /// Returns the value of the key if found, otherwise returns the default value.
+	    /// </summary>
+	    /// <param name="key">The key to search for.</param>
+	    /// <param name="defaultValue">The default value to return if the key is not found.</param>
+	    /// <param name="section">The section of the config to search the key for.</param>
+	    /// <returns>The value of the key.</returns>
+	    public static string GetEntry(string key, string defaultValue = "", string section = "")
         {
             key = Sanitize(key);
             if (section.IsNullOrWhiteSpace())
@@ -196,26 +200,31 @@ namespace BepInEx
             return true;
         }
 
-        public static string Sanitize(string key)
+		/// <summary>
+		/// Replaces any potentially breaking input with underscores.
+		/// </summary>
+		/// <param name="text">The text to sanitize.</param>
+		/// <returns>Sanitized text.</returns>
+        public static string Sanitize(string text)
         {
-            return sanitizeKeyRegex.Replace(key, "_");
+            return sanitizeKeyRegex.Replace(text, "_");
         }
 
         #region Extensions
 
         public static string GetEntry(this BaseUnityPlugin plugin, string key, string defaultValue = "")
         {
-            return GetEntry(key, defaultValue, TypeLoader.GetMetadata(plugin).GUID);
+            return GetEntry(key, defaultValue, MetadataHelper.GetMetadata(plugin).GUID);
         }
 
         public static void SetEntry(this BaseUnityPlugin plugin, string key, string value)
         {
-            SetEntry(key, value, TypeLoader.GetMetadata(plugin).GUID);
+            SetEntry(key, value, MetadataHelper.GetMetadata(plugin).GUID);
         }
 
         public static bool HasEntry(this BaseUnityPlugin plugin, string key)
         {
-            return HasEntry(key, TypeLoader.GetMetadata(plugin).GUID);
+            return HasEntry(key, MetadataHelper.GetMetadata(plugin).GUID);
         }
         #endregion Extensions
     }

+ 6 - 5
BepInEx/ConfigWrapper.cs

@@ -1,5 +1,6 @@
 using System;
 using System.ComponentModel;
+using BepInEx.Logging;
 
 namespace BepInEx
 {
@@ -69,19 +70,19 @@ namespace BepInEx
         public ConfigWrapper(string key, BaseUnityPlugin plugin, T @default = default(T))
             : this(key, @default)
         {
-            Section = TypeLoader.GetMetadata(plugin).GUID;
+            Section = MetadataHelper.GetMetadata(plugin).GUID;
         }
 
         public ConfigWrapper(string key, BaseUnityPlugin plugin, Func<string, T> strToObj, Func<T, string> objToStr, T @default = default(T))
           : this(key, strToObj, objToStr, @default)
         {
-            Section = TypeLoader.GetMetadata(plugin).GUID;
+            Section = MetadataHelper.GetMetadata(plugin).GUID;
         }
 
         public ConfigWrapper(string key, BaseUnityPlugin plugin, IConfigConverter<T> converter, T @default = default(T))
           : this(key, converter.ConvertFromString, converter.ConvertToString, @default)
         {
-            Section = TypeLoader.GetMetadata(plugin).GUID;
+            Section = MetadataHelper.GetMetadata(plugin).GUID;
         }
 
         public ConfigWrapper(string key, string section, T @default = default(T))
@@ -116,7 +117,7 @@ namespace BepInEx
             }
             catch (Exception ex)
             {
-                BepInLogger.Log("ConfigWrapper Get Converter Exception: " + ex.Message);
+                Logger.Log(LogLevel.Error, "ConfigWrapper Get Converter Exception: " + ex.Message);
                 return _default;
             }
         }
@@ -130,7 +131,7 @@ namespace BepInEx
             }
             catch (Exception ex)
             {
-                BepInLogger.Log("ConfigWrapper Set Converter Exception: " + ex.Message);
+                Logger.Log(LogLevel.Error, "ConfigWrapper Set Converter Exception: " + ex.Message);
             }
         }
 

+ 0 - 84
BepInEx/ConsoleUtil/ConsoleMirror.cs

@@ -1,84 +0,0 @@
-// --------------------------------------------------
-// UnityInjector - ConsoleMirror.cs
-// Copyright (c) Usagirei 2015 - 2015
-// --------------------------------------------------
-
-using System;
-using System.Diagnostics;
-using System.IO;
-using System.Text;
-
-namespace UnityInjector.ConsoleUtil
-{
-    internal class ConsoleMirror : IDisposable
-    {
-        private readonly MirrorWriter _tWriter;
-
-        public ConsoleMirror(string path)
-        {
-            try
-            {
-                var fileStream = File.Open(path, FileMode.Append, FileAccess.Write, FileShare.Read);
-                var fileWriter = new StreamWriter(fileStream)
-                {
-                    AutoFlush = true
-                };
-                _tWriter = new MirrorWriter(fileWriter, Console.Out);
-            }
-            catch (Exception e)
-            {
-                SafeConsole.ForegroundColor = ConsoleColor.DarkGray;
-                Console.WriteLine("Couldn't open file to write: {0}", Path.GetFileName(path));
-                Console.WriteLine(e.Message);
-                SafeConsole.ForegroundColor = ConsoleColor.Gray;
-
-                return;
-            }
-            Console.SetOut(_tWriter);
-
-            Console.WriteLine();
-
-            var processName = Process.GetCurrentProcess().ProcessName;
-            var now = DateTime.Now;
-
-            Console.WriteLine($" {processName} - {now:dd-MM-yyyy hh:mm:ss} ".PadCenter(79, '='));
-            Console.WriteLine();
-        }
-
-        public void Dispose()
-        {
-            var cOld = _tWriter.Console;
-            var fOld = _tWriter.File;
-            Console.SetOut(cOld);
-            if (fOld == null)
-                return;
-            fOld.Flush();
-            fOld.Close();
-        }
-
-        private class MirrorWriter : TextWriter
-        {
-            public TextWriter Console { get; }
-            public override Encoding Encoding => File.Encoding;
-            public TextWriter File { get; }
-
-            public MirrorWriter(TextWriter file, TextWriter console)
-            {
-                File = file;
-                Console = console;
-            }
-
-            public override void Flush()
-            {
-                File.Flush();
-                Console.Flush();
-            }
-
-            public override void Write(char value)
-            {
-                File.Write(value);
-                Console.Write(value);
-            }
-        }
-    }
-}

+ 26 - 0
BepInEx/ConsoleUtil/ConsoleWindow.cs

@@ -43,6 +43,29 @@ namespace UnityInjector.ConsoleUtil
             _attached = true;
         }
 
+        public static string Title
+        {
+            set
+            {
+                if (_attached)
+                {
+                    if (value == null)
+                    {
+                        throw new ArgumentNullException(nameof(value));
+                    }
+                    if (value.Length > 24500)
+                    {
+                        throw new InvalidOperationException("Console title too long");
+                    }
+
+                    if (!SetConsoleTitle(value))
+                    {
+                        throw new InvalidOperationException("Console title invalid");
+                    }
+                }
+            }
+        }
+
         public static void Detach()
         {
             if (!_attached)
@@ -104,5 +127,8 @@ namespace UnityInjector.ConsoleUtil
 
         [DllImport("kernel32.dll", SetLastError = true)]
         private static extern bool SetStdHandle(int nStdHandle, IntPtr hConsoleOutput);
+        
+        [DllImport("kernel32.dll", BestFitMapping = true, CharSet = CharSet.Auto, SetLastError = true)]
+        private static extern bool SetConsoleTitle(string title);
     }
 }

+ 0 - 34
BepInEx/ConsoleUtil/Extensions.cs

@@ -1,34 +0,0 @@
-// --------------------------------------------------
-// UnityInjector - Extensions.cs
-// Copyright (c) Usagirei 2015 - 2015
-// --------------------------------------------------
-
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Text.RegularExpressions;
-
-namespace UnityInjector
-{
-    internal static class Extensions
-    {
-        public static string PluginsPath { get; } = Path.Combine(Environment.CurrentDirectory, "UnityInjector");
-        public static string UserDataPath { get; } = Path.Combine(PluginsPath, "Config");
-        public static string Asciify(this string s) => Regex.Replace(s, @"[^0-9A-Za-z]", "_");
-        public static string CombinePaths(params string[] parts) => parts.Aggregate(Path.Combine);
-
-        public static void ForEach<T>(this IEnumerable<T> tees, Action<T> action)
-        {
-            foreach (var tee in tees)
-                action(tee);
-        }
-
-        public static string PadCenter(this string str, int totalWidth, char paddingChar = ' ')
-        {
-            int spaces = totalWidth - str.Length;
-            int padLeft = spaces / 2 + str.Length;
-            return str.PadLeft(padLeft, paddingChar).PadRight(totalWidth, paddingChar);
-        }
-    }
-}

+ 199 - 0
BepInEx/Contract/Attributes.cs

@@ -0,0 +1,199 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace BepInEx
+{
+    #region BaseUnityPlugin
+
+    /// <summary>
+    /// This attribute denotes that a class is a plugin, and specifies the required metadata.
+    /// </summary>
+    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
+    public class BepInPlugin : Attribute
+    {
+        /// <summary>
+        /// The unique identifier of the plugin. Should not change between plugin versions.
+        /// </summary>
+        public string GUID { get; protected set; }
+
+        
+        /// <summary>
+        /// The user friendly name of the plugin. Is able to be changed between versions.
+        /// </summary>
+        public string Name { get; protected set; }
+
+        
+        /// <summary>
+        /// The specfic version of the plugin.
+        /// </summary>
+        public Version Version { get; protected set; }
+        
+        /// <param name="GUID">The unique identifier of the plugin. Should not change between plugin versions.</param>
+        /// <param name="Name">The user friendly name of the plugin. Is able to be changed between versions.</param>
+        /// <param name="Version">The specfic version of the plugin.</param>
+        public BepInPlugin(string GUID, string Name, string Version)
+        {
+            this.GUID = GUID;
+            this.Name = Name;
+            this.Version = new Version(Version);
+        }
+    }
+
+    /// <summary>
+    /// This attribute specifies any dependencies that this plugin has on other plugins.
+    /// </summary>
+    [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
+    public class BepInDependency : Attribute
+    {
+        public enum DependencyFlags
+        {
+            /// <summary>
+            /// The plugin has a hard dependency on the referenced plugin, and will not run without it.
+            /// </summary>
+            HardDependency = 1,
+
+            /// <summary>
+            /// This plugin has a soft dependency on the referenced plugin, and is able to run without it.
+            /// </summary>
+            SoftDependency = 2,
+        }
+
+        /// <summary>
+        /// The GUID of the referenced plugin.
+        /// </summary>
+        public string DependencyGUID { get; protected set; }
+
+        /// <summary>
+        /// The flags associated with this dependency definition.
+        /// </summary>
+        public DependencyFlags Flags { get; protected set; }
+        
+        /// <param name="DependencyGUID">The GUID of the referenced plugin.</param>
+        /// <param name="Flags">The flags associated with this dependency definition.</param>
+        public BepInDependency(string DependencyGUID, DependencyFlags Flags = DependencyFlags.HardDependency)
+        {
+            this.DependencyGUID = DependencyGUID;
+            this.Flags = Flags;
+        }
+    }
+
+    /// <summary>
+    /// This attribute specifies which processes this plugin should be run for. Not specifying this attribute will load the plugin under every process.
+    /// </summary>
+    [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
+    public class BepInProcess : Attribute
+    {
+        /// <summary>
+        /// The name of the process that this plugin will run under.
+        /// </summary>
+        public string ProcessName { get; protected set; }
+        
+        /// <param name="ProcessName">The name of the process that this plugin will run under.</param>
+        public BepInProcess(string ProcessName)
+        {
+            this.ProcessName = ProcessName;
+        }
+    }
+
+    #endregion
+
+    #region MetadataHelper
+
+	/// <summary>
+	/// Helper class to use for retrieving metadata about a plugin, defined as attributes.
+	/// </summary>
+    public static class MetadataHelper
+    {
+		/// <summary>
+		/// Retrieves the BepInPlugin metadata from a plugin instance.
+		/// </summary>
+		/// <param name="plugin">The plugin instance.</param>
+		/// <returns>The BepInPlugin metadata of the plugin instance.</returns>
+        public static BepInPlugin GetMetadata(object plugin)
+        {
+            return GetMetadata(plugin.GetType());
+        }
+		
+	    /// <summary>
+	    /// Retrieves the BepInPlugin metadata from a plugin type.
+	    /// </summary>
+	    /// <param name="plugin">The plugin type.</param>
+	    /// <returns>The BepInPlugin metadata of the plugin type.</returns>
+        public static BepInPlugin GetMetadata(Type pluginType)
+        {
+            object[] attributes = pluginType.GetCustomAttributes(typeof(BepInPlugin), false);
+
+            if (attributes.Length == 0)
+                return null;
+
+            return (BepInPlugin)attributes[0];
+        }
+
+		/// <summary>
+		/// Gets the specified attributes of an instance, if they exist.
+		/// </summary>
+		/// <typeparam name="T">The attribute type to retrieve.</typeparam>
+		/// <param name="plugin">The plugin instance.</param>
+		/// <returns>The attributes of the instance, if existing.</returns>
+        public static IEnumerable<T> GetAttributes<T>(object plugin) where T : Attribute
+        {
+            return GetAttributes<T>(plugin.GetType());
+        }
+		
+	    /// <summary>
+	    /// Gets the specified attributes of a type, if they exist.
+	    /// </summary>
+	    /// <typeparam name="T">The attribute type to retrieve.</typeparam>
+	    /// <param name="plugin">The plugin type.</param>
+	    /// <returns>The attributes of the type, if existing.</returns>
+        public static IEnumerable<T> GetAttributes<T>(Type pluginType) where T : Attribute
+        {
+            return pluginType.GetCustomAttributes(typeof(T), true).Cast<T>();
+        }
+
+		/// <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<Type> GetDependencies(Type Plugin, IEnumerable<Type> AllPlugins)
+        {
+            object[] attributes = Plugin.GetCustomAttributes(typeof(BepInDependency), true);
+
+            List<Type> dependencyTypes = new List<Type>();
+
+            foreach (BepInDependency dependency in attributes)
+            {
+                Type dependencyType = AllPlugins.FirstOrDefault(x => GetMetadata(x)?.GUID == dependency.DependencyGUID);
+
+                if (dependencyType == null)
+                {
+                    if ((dependency.Flags & BepInDependency.DependencyFlags.SoftDependency) != 0)
+                        continue; //skip on soft dependencies
+
+                    throw new MissingDependencyException("Cannot find dependency type.");
+                }
+                    
+
+                dependencyTypes.Add(dependencyType);
+            }
+
+            return dependencyTypes;
+        }
+    }
+
+	/// <summary>
+	/// An exception which is thrown when a plugin's dependencies cannot be found.
+	/// </summary>
+    public class MissingDependencyException : Exception
+    {
+        public MissingDependencyException(string message) : base(message)
+        {
+
+        }
+    }
+
+    #endregion
+}

+ 2 - 3
BepInEx/BaseUnityPlugin.cs

@@ -1,10 +1,9 @@
-using System;
-using UnityEngine;
+using UnityEngine;
 
 namespace BepInEx
 {
     /// <summary>
-    /// The base plugin type, that is loaded into the game.
+    /// The base plugin type that is used by the BepInEx plugin loader.
     /// </summary>
     public abstract class BaseUnityPlugin : MonoBehaviour
     {

+ 7 - 19
BepInEx/Logger.cs

@@ -1,12 +1,12 @@
 using System;
-using System.Runtime.CompilerServices;
-using BepInEx.ConsoleUtil;
+using BepInEx.Logging;
 
 namespace BepInEx
 {
     /// <summary>
     /// A helper class to use for logging.
     /// </summary>
+    [Obsolete("This class has been deprecated; please use the Logger static class and BaseLogger implementations", true)]
     public static class BepInLogger
     {
         /// <summary>
@@ -28,7 +28,9 @@ namespace BepInEx
         /// <param name="show">Whether or not it should be dislpayed to the user.</param>
         public static void Log(string entry, bool show = false)
         {
-            Log(entry, show, ConsoleColor.Gray);
+	        Logger.Log(show ? LogLevel.Message : LogLevel.Info, entry);
+
+	        EntryLogged?.Invoke(entry, show);
         }
 
         /// <summary>
@@ -39,7 +41,7 @@ namespace BepInEx
         /// <param name="color">The color of the text to show in the console.</param>
         public static void Log(object entry, bool show, ConsoleColor color)
         {
-            Log(entry.ToString(), show, color);
+	        Log(entry.ToString(), show);
         }
 
         /// <summary>
@@ -50,21 +52,7 @@ namespace BepInEx
         /// <param name="color">The color of the text to show in the console.</param>
         public static void Log(string entry, bool show, ConsoleColor color)
         {
-            UnityEngine.UnityLogWriter.WriteStringToUnityLog($"BEPIN - {entry}\r\n");
-
-            Kon.ForegroundColor = color;
-            Console.WriteLine(entry);
-
-            EntryLogged?.Invoke(entry, show);
+			Log(entry, show);
         }
     }
-}
-
-namespace UnityEngine
-{
-    internal sealed class UnityLogWriter
-    {
-        [MethodImpl(MethodImplOptions.InternalCall)]
-        public static extern void WriteStringToUnityLog(string s);
-    }
 }

+ 99 - 0
BepInEx/Logging/BaseLogger.cs

@@ -0,0 +1,99 @@
+using System;
+using System.IO;
+using System.Text;
+
+namespace BepInEx.Logging
+{
+	/// <summary>
+	/// The base implementation of a logging class.
+	/// </summary>
+    public abstract class BaseLogger : TextWriter
+    {
+	    protected BaseLogger()
+	    {
+		    DisplayedLevels = GetDefaultLogLevel();
+	    }
+
+		/// <summary>
+		/// The encoding that the underlying text writer should use. Defaults to UTF-8 BOM.
+		/// </summary>
+        public override Encoding Encoding { get; } = new UTF8Encoding(true);
+
+
+        /// <summary>
+        /// The handler for a entry logged event.
+        /// </summary>
+        /// <param name="entry">The text element of the log itself.</param>
+        /// <param name="show">Whether or not it should be dislpayed to the user.</param>
+        public delegate void EntryLoggedEventHandler(LogLevel level, object entry);
+
+        /// <summary>
+        /// The listener event for an entry being logged.
+        /// </summary>
+        public event EntryLoggedEventHandler EntryLogged;
+
+		/// <summary>
+		/// Retrieves the default log level to use for this logger.
+		/// </summary>
+		/// <returns>The default log level to use.</returns>
+	    protected virtual LogLevel GetDefaultLogLevel()
+		{
+			LogLevel lowest;
+
+		    try
+		    {
+			    lowest = (LogLevel)Enum.Parse(typeof(LogLevel), Config.GetEntry("logger-displayed-levels", nameof(LogLevel.Info)));
+		    }
+		    catch
+		    {
+			    return LogLevel.All;
+		    }
+
+		    if (lowest == LogLevel.None)
+			    return LogLevel.None;
+
+		    LogLevel value = lowest;
+
+		    foreach (LogLevel e in Enum.GetValues(typeof(LogLevel)))
+		    {
+			    if (lowest > e)
+				    value |= e;
+		    }
+
+		    return value;
+	    }
+
+        /// <summary>
+		/// A filter which is used to specify which log levels are not ignored by the logger.
+		/// </summary>
+        public LogLevel DisplayedLevels { get; set; }
+
+        private readonly object logLockObj = new object();
+
+		/// <summary>
+		/// Logs an entry to the Logger instance.
+		/// </summary>
+		/// <param name="level">The level of the entry.</param>
+		/// <param name="entry">The textual value of the entry.</param>
+        public virtual void Log(LogLevel level, object entry)
+        {
+            if ((DisplayedLevels & level) != LogLevel.None)
+            {
+                lock (logLockObj)
+                {
+                    EntryLogged?.Invoke(level, entry);
+                    WriteLine($"[{level.GetHighestLevel()}] {entry}");
+                }
+            }
+        }
+
+		/// <summary>
+		/// Logs an entry to the Logger instance, with a <see cref="LogLevel"/> of Message.
+		/// </summary>
+		/// <param name="entry">The text value of this log entry.</param>
+        public virtual void Log(object entry)
+        {
+            Log(LogLevel.Message, entry);
+        }
+    }
+}

+ 100 - 0
BepInEx/Logging/LogLevel.cs

@@ -0,0 +1,100 @@
+using System;
+
+namespace BepInEx.Logging
+{
+	/// <summary>
+	/// The level, or severity of a log entry.
+	/// </summary>
+    [Flags]
+    public enum LogLevel
+    {
+		/// <summary>
+		/// No level selected.
+		/// </summary>
+        None = 0,
+
+		/// <summary>
+		/// A fatal error has occurred, which cannot be recovered from.
+		/// </summary>
+        Fatal = 1,
+
+		/// <summary>
+		/// An error has occured, but can be recovered from.
+		/// </summary>
+        Error = 2,
+
+		/// <summary>
+		/// A warning has been produced, but does not necessarily mean that something wrong has happened.
+		/// </summary>
+        Warning = 4,
+
+		/// <summary>
+		/// An important message that should be displayed to the user.
+		/// </summary>
+        Message = 8,
+
+		/// <summary>
+		/// A message of low importance.
+		/// </summary>
+        Info = 16,
+
+		/// <summary>
+		/// A message that would likely only interest a developer.
+		/// </summary>
+        Debug = 32,
+
+		/// <summary>
+		/// All log levels.
+		/// </summary>
+        All = Fatal | Error | Warning | Message | Info | Debug
+    }
+
+    public static class LogLevelExtensions
+    {
+		/// <summary>
+		/// Gets the highest log level when there could potentially be multiple levels provided.
+		/// </summary>
+		/// <param name="levels">The log level(s).</param>
+		/// <returns>The highest log level supplied.</returns>
+        public static LogLevel GetHighestLevel(this LogLevel levels)
+        {
+            var enums = Enum.GetValues(typeof(LogLevel));
+            Array.Sort(enums);
+
+            foreach (LogLevel e in enums)
+            {
+                if ((levels & e) != LogLevel.None)
+                    return e;
+            }
+
+            return LogLevel.None;
+        }
+
+		/// <summary>
+		/// Returns a translation of a log level to it's associated console colour.
+		/// </summary>
+		/// <param name="level">The log level(s).</param>
+		/// <returns>A console color associated with the highest log level supplied.</returns>
+        public static ConsoleColor GetConsoleColor(this LogLevel level)
+        {
+            level = GetHighestLevel(level);
+
+            switch (level)
+            {
+                case LogLevel.Fatal:
+                    return ConsoleColor.Red;
+                case LogLevel.Error:
+                    return ConsoleColor.DarkRed;
+                case LogLevel.Warning:
+                    return ConsoleColor.Yellow;
+                case LogLevel.Message:
+                    return ConsoleColor.White;
+                case LogLevel.Info:
+                default:
+                    return ConsoleColor.Gray;
+                case LogLevel.Debug:
+                    return ConsoleColor.DarkGray;
+            }
+        }
+    }
+}

+ 43 - 0
BepInEx/Logging/Logger.cs

@@ -0,0 +1,43 @@
+using BepInEx.Logging;
+
+namespace BepInEx
+{
+	/// <summary>
+	/// A static <see cref="BaseLogger"/> instance.
+	/// </summary>
+    public static class Logger
+    {
+		/// <summary>
+		/// The current instance of a <see cref="BaseLogger"/> that is being used.
+		/// </summary>
+        public static BaseLogger CurrentLogger { get; set; }
+
+        /// <summary>
+        /// The listener event for an entry being logged.
+        /// </summary>
+        public static event BaseLogger.EntryLoggedEventHandler EntryLogged;
+
+		/// <summary>
+		/// Logs an entry to the current logger instance.
+		/// </summary>
+		/// <param name="level">The level of the entry.</param>
+		/// <param name="entry">The textual value of the entry.</param>
+        public static void Log(LogLevel level, object entry)
+        {
+            EntryLogged?.Invoke(level, entry);
+
+            CurrentLogger?.Log(level, entry);
+        }
+
+		/// <summary>
+		/// Sets the instance being used by the static <see cref="Logger"/> class.
+		/// </summary>
+		/// <param name="logger">The instance to use in the static class.</param>
+        public static void SetLogger(BaseLogger logger)
+        {
+            CurrentLogger?.Dispose();
+
+            CurrentLogger = logger;
+        }
+    }
+}

+ 151 - 0
BepInEx/Logging/LoggerTraceListener.cs

@@ -0,0 +1,151 @@
+using System;
+using System.Diagnostics;
+using System.Linq;
+using System.Reflection;
+using Harmony;
+
+namespace BepInEx.Logging
+{
+	/// <summary>
+	/// A trace listener that writes to an underlying <see cref="BaseLogger"/> instance.
+	/// </summary>
+	/// <inheritdoc cref="TraceListener"/>
+    public class LoggerTraceListener : TraceListener
+    {
+		/// <summary>
+		/// The logger instance that is being written to.
+		/// </summary>
+        public BaseLogger Logger { get; }
+
+        static LoggerTraceListener()
+        {
+            try
+            {
+                TraceFixer.ApplyFix();
+            }
+            catch { } //ignore everything, if it's thrown an exception, we're using an assembly that has already fixed this
+        }
+		
+		/// <param name="logger">The logger instance to write to.</param>
+        public LoggerTraceListener(BaseLogger logger)
+        {
+            Logger = logger;
+        }
+		
+		/// <summary>
+		/// Writes a message to the underlying <see cref="BaseLogger"/> instance.
+		/// </summary>
+		/// <param name="message">The message to write.</param>
+        public override void Write(string message)
+        {
+            Logger.Write(message);
+        }
+		
+	    /// <summary>
+	    /// Writes a message and a newline to the underlying <see cref="BaseLogger"/> instance.
+	    /// </summary>
+	    /// <param name="message">The message to write.</param>
+        public override void WriteLine(string message)
+        {
+            Logger.WriteLine(message);
+        }
+
+        public override void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id, string format, params object[] args)
+            => TraceEvent(eventCache, source, eventType, id, string.Format(format, args));
+
+        public override void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id, string message)
+        {
+            LogLevel level;
+
+            switch (eventType)
+            {
+                case TraceEventType.Critical:
+                    level = LogLevel.Fatal;
+                    break;
+                case TraceEventType.Error:
+                    level = LogLevel.Error;
+                    break;
+                case TraceEventType.Warning:
+                    level = LogLevel.Warning;
+                    break;
+                case TraceEventType.Information:
+                    level = LogLevel.Info;
+                    break;
+                case TraceEventType.Verbose:
+                default:
+                    level = LogLevel.Debug;
+                    break;
+            }
+
+            Logger.Log(level, $"{source} : {message}");
+        }
+
+		/// <summary>
+		/// This exists because the Mono implementation of <see cref="Trace"/> is/was broken, and would call Write directly instead of calling TraceEvent. This class fixes that with a <see cref="Harmony"/> hook.
+		/// </summary>
+        private static class TraceFixer
+        {
+            private static Type TraceImplType;
+            
+            private static object ListenersSyncRoot;
+            private static TraceListenerCollection Listeners;
+            private static PropertyInfo prop_AutoFlush;
+
+            private static bool AutoFlush => (bool)prop_AutoFlush.GetValue(null, null);
+
+
+            public static void ApplyFix()
+            {
+                TraceImplType = AppDomain.CurrentDomain.GetAssemblies()
+                    .First(x => x.GetName().Name == "System")
+                    .GetTypes()
+                    .First(x => x.Name == "TraceImpl");
+
+                
+                ListenersSyncRoot = AccessTools.Property(TraceImplType, "ListenersSyncRoot").GetValue(null, null);
+                
+                Listeners = (TraceListenerCollection)AccessTools.Property(TraceImplType, "Listeners").GetValue(null, null);
+                
+                prop_AutoFlush = AccessTools.Property(TraceImplType, "AutoFlush");
+
+
+
+                HarmonyInstance instance = HarmonyInstance.Create("com.bepis.bepinex.tracefix");
+
+                instance.Patch(
+                    typeof(Trace).GetMethod("DoTrace", BindingFlags.Static | BindingFlags.NonPublic),
+                    new HarmonyMethod(typeof(TraceFixer).GetMethod(nameof(DoTraceReplacement), BindingFlags.Static | BindingFlags.Public)),
+                    null);
+            }
+
+
+            public static bool DoTraceReplacement(string kind, Assembly report, string message)
+            {
+                string arg = string.Empty;
+                try
+                {
+                    arg = report.GetName().Name;
+                }
+                catch (MethodAccessException) { }
+
+                TraceEventType type = (TraceEventType)Enum.Parse(typeof(TraceEventType), kind);
+
+                lock (ListenersSyncRoot)
+                {
+                    foreach (object obj in Listeners)
+                    {
+                        TraceListener traceListener = (TraceListener)obj;
+                        traceListener.TraceEvent(new TraceEventCache(), arg, type, 0, message);
+
+                        if (AutoFlush)
+                        {
+                            traceListener.Flush();
+                        }
+                    }
+                }
+
+                return false;
+            }
+        }
+    }
+}

+ 127 - 0
BepInEx/Logging/PreloaderLogWriter.cs

@@ -0,0 +1,127 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Text;
+using BepInEx.Bootstrap;
+using BepInEx.ConsoleUtil;
+
+namespace BepInEx.Logging
+{
+	/// <summary>
+	/// A log writer specific to the <see cref="Preloader"/>.
+	/// </summary>
+	/// <inheritdoc cref="BaseLogger"/>
+    public class PreloaderLogWriter : BaseLogger
+    {
+		/// <summary>
+		/// The <see cref="System.Text.StringBuilder"/> which contains all logged entries, so it may be passed onto another log writer.
+		/// </summary>
+        public StringBuilder StringBuilder { get; protected set; } = new StringBuilder();
+
+		/// <summary>
+		/// Whether or not the log writer is redirecting console output, so it can be logged.
+		/// </summary>
+        public bool IsRedirectingConsole { get; protected set; }
+
+        protected TextWriter stdout;
+        protected LoggerTraceListener traceListener;
+
+        private bool _enabled = false;
+
+		/// <summary>
+		/// Whether or not the log writer is writing and/or redirecting.
+		/// </summary>
+        public bool Enabled {
+            get => _enabled;
+            set
+            {
+                if (value)
+                    Enable();
+                else
+                    Disable();
+            }
+        }
+		
+		/// <param name="redirectConsole">Whether or not to redirect the console standard output.</param>
+        public PreloaderLogWriter(bool redirectConsole)
+        {
+            IsRedirectingConsole = redirectConsole;
+            
+            stdout = Console.Out;
+            traceListener = new LoggerTraceListener(this);
+        }
+
+		/// <summary>
+		/// Enables the log writer.
+		/// </summary>
+        public void Enable()
+        {
+            if (_enabled)
+                return;
+
+            if (IsRedirectingConsole)
+                Console.SetOut(this);
+            else
+                Console.SetOut(TextWriter.Null);
+
+            Trace.Listeners.Add(traceListener);
+
+            _enabled = true;
+        }
+
+		/// <summary>
+		/// Disables the log writer.
+		/// </summary>
+        public void Disable()
+        {
+            if (!_enabled)
+                return;
+            
+            Console.SetOut(stdout);
+
+            Trace.Listeners.Remove(traceListener);
+
+            _enabled = false;
+        }
+		
+	    /// <summary>
+	    /// Logs an entry to the Logger instance.
+	    /// </summary>
+	    /// <param name="level">The level of the entry.</param>
+	    /// <param name="entry">The textual value of the entry.</param>
+        public override void Log(LogLevel level, object entry)
+        {
+            Kon.ForegroundColor = level.GetConsoleColor();
+            base.Log(level, entry);
+            Kon.ForegroundColor = ConsoleColor.Gray;
+        }
+
+        public override void Write(char value)
+        {
+            StringBuilder.Append(value);
+
+            stdout.Write(value);
+        }
+
+        public override void Write(string value)
+        {
+            StringBuilder.Append(value);
+
+            stdout.Write(value);
+        }
+
+        protected override void Dispose(bool disposing)
+        {
+            Disable();
+            StringBuilder.Length = 0;
+            
+            traceListener?.Dispose();
+            traceListener = null;
+        }
+
+        public override string ToString()
+        {
+            return StringBuilder.ToString().Trim();
+        }
+    }
+}

+ 52 - 0
BepInEx/Logging/UnityLogWriter.cs

@@ -0,0 +1,52 @@
+using System;
+using System.Runtime.CompilerServices;
+using BepInEx.ConsoleUtil;
+
+namespace BepInEx.Logging
+{
+	/// <summary>
+	/// Logs entries using Unity specific outputs.
+	/// </summary>
+    public class UnityLogWriter : BaseLogger
+    {
+		/// <summary>
+		/// Writes a string specifically to the game output log.
+		/// </summary>
+		/// <param name="value">The value to write.</param>
+        public void WriteToLog(string value)
+        {
+            UnityEngine.UnityLogWriter.WriteStringToUnityLog(value);
+        }
+
+        protected void InternalWrite(string value)
+        {
+            Console.Write(value);
+            WriteToLog(value);
+        }
+
+	    /// <summary>
+	    /// Logs an entry to the Logger instance.
+	    /// </summary>
+	    /// <param name="level">The level of the entry.</param>
+	    /// <param name="entry">The textual value of the entry.</param>
+        public override void Log(LogLevel level, object entry)
+        {
+            Kon.ForegroundColor = level.GetConsoleColor();
+            base.Log(level, entry);
+            Kon.ForegroundColor = ConsoleColor.Gray;
+        }
+
+        public override void WriteLine(string value) => InternalWrite($"{value}\r\n");
+        public override void Write(char value) => InternalWrite(value.ToString());
+        public override void Write(string value) => InternalWrite(value);
+    }
+}
+
+namespace UnityEngine
+{
+    internal sealed class UnityLogWriter
+    {
+        [MethodImpl(MethodImplOptions.InternalCall)]
+        public static extern void WriteStringToUnityLog(string s);
+    }
+}

+ 2 - 2
BepInEx/Properties/AssemblyInfo.cs

@@ -32,5 +32,5 @@ using System.Runtime.InteropServices;
 // You can specify all the values or you can default the Build and Revision Numbers
 // by using the '*' as shown below:
 // [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("3.2.0.0")]
-[assembly: AssemblyFileVersion("3.2.0.0")]
+[assembly: AssemblyVersion("4.0.0.0")]
+[assembly: AssemblyFileVersion("4.0.0.0")]

+ 0 - 116
BepInEx/TypeLoader.cs

@@ -1,116 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Reflection;
-using System.Text;
-
-namespace BepInEx
-{
-    public static class TypeLoader
-    {
-        /// <summary>
-        /// Loads a list of types from a directory containing assemblies, that derive from a base type.
-        /// </summary>
-        /// <typeparam name="T">The specfiic 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)
-        {
-            List<Type> types = new List<Type>();
-            Type pluginType = typeof(T);
-
-            foreach (string dll in Directory.GetFiles(Path.GetFullPath(directory), "*.dll"))
-            {
-                try
-                {
-                    AssemblyName an = AssemblyName.GetAssemblyName(dll);
-                    Assembly assembly = Assembly.Load(an);
-
-                    foreach (Type type in assembly.GetTypes())
-                    {
-                        if (type.IsInterface || type.IsAbstract)
-                        {
-                            continue;
-                        }
-                        else
-                        {
-                            if (type.BaseType == pluginType)
-                                types.Add(type);
-                        }
-                    }
-                }
-                catch (BadImageFormatException) { } //unmanaged DLL
-                catch (ReflectionTypeLoadException)
-                {
-                    BepInLogger.Log($"ERROR! Could not load \"{Path.GetFileName(dll)}\" as a plugin!");
-                } 
-            }
-
-            return types;
-        }
-
-        public static BepInPlugin GetMetadata(object plugin)
-        {
-            return GetMetadata(plugin.GetType());
-        }
-
-        public static BepInPlugin GetMetadata(Type pluginType)
-        {
-            object[] attributes = pluginType.GetCustomAttributes(typeof(BepInPlugin), false);
-
-            if (attributes.Length == 0)
-                return null;
-
-            return (BepInPlugin)attributes[0];
-        }
-
-        public static IEnumerable<T> GetAttributes<T>(object plugin) where T : Attribute
-        {
-            return GetAttributes<T>(plugin.GetType());
-        }
-
-        public static IEnumerable<T> GetAttributes<T>(Type pluginType) where T : Attribute
-        {
-            return pluginType.GetCustomAttributes(typeof(T), true).Cast<T>();
-        }
-
-        public static IEnumerable<Type> GetDependencies(Type Plugin, IEnumerable<Type> AllPlugins)
-        {
-            object[] attributes = Plugin.GetCustomAttributes(typeof(BepInDependency), true);
-
-            List<Type> dependencyTypes = new List<Type>();
-
-            foreach (BepInDependency dependency in attributes)
-            {
-                Type dependencyType = AllPlugins.FirstOrDefault(x => GetMetadata(x)?.GUID == dependency.DependencyGUID);
-
-                if (dependencyType == null)
-                {
-                    if ((dependency.Flags & BepInDependency.DependencyFlags.SoftDependency) != 0)
-                        continue; //skip on soft dependencies
-
-                    throw new MissingDependencyException("Cannot find dependency type.");
-                }
-                    
-
-                dependencyTypes.Add(dependencyType);
-            }
-
-            return dependencyTypes;
-        }
-    }
-
-    public class MissingDependencyException : Exception
-    {
-        public MissingDependencyException() : base()
-        {
-
-        }
-
-        public MissingDependencyException(string message) : base(message)
-        {
-
-        }
-    }
-}

+ 5 - 0
doorstop/doorstop_config.ini

@@ -0,0 +1,5 @@
+[UnityDoorstop]
+# Specifies whether assembly executing is enabled
+enabled=true
+# Specifies the path (absolute, or relative to the game's exe) to the DLL/EXE that should be executed by Doorstop
+targetAssembly=BepInEx\core\BepInEx.dll

BIN
lib/Cecil 10/Mono.Cecil.dll


BIN
lib/Cecil 9/Mono.Cecil.dll