Browse Source

Merge branch 'framework-il2cpp-rebase' into framework-il2cpp

Bepis 3 years ago
parent
commit
f9ec1e9ce4
66 changed files with 1902 additions and 593 deletions
  1. 6 6
      .github/ISSUE_TEMPLATE/bug_report.md
  2. 4 4
      .github/ISSUE_TEMPLATE/feature_request.md
  3. 28 0
      .github/pull_request_template.md
  4. 26 7
      BepInEx.Core/BepInEx.Core.csproj
  5. 47 23
      BepInEx.Core/Bootstrap/BaseChainloader.cs
  6. 22 7
      BepInEx.Core/Bootstrap/TypeLoader.cs
  7. 4 4
      BepInEx.Core/Configuration/ConfigFile.cs
  8. 133 0
      BepInEx.Core/Console/ConsoleManager.cs
  9. 26 0
      BepInEx.Core/Console/IConsoleDriver.cs
  10. 46 9
      BepInEx.Core/ConsoleUtil/SafeConsole.cs
  11. 57 0
      BepInEx.Core/Console/Unix/ConsoleWriter.cs
  12. 112 0
      BepInEx.Core/Console/Unix/LinuxConsoleDriver.cs
  13. 214 0
      BepInEx.Core/Console/Unix/TtyHandler.cs
  14. 85 0
      BepInEx.Core/Console/Unix/UnixStream.cs
  15. 60 0
      BepInEx.Core/Console/Unix/UnixStreamHelper.cs
  16. 0 0
      BepInEx.Core/Console/Windows/ConsoleEncoding/ConsoleEncoding.Buffers.cs
  17. 0 0
      BepInEx.Core/Console/Windows/ConsoleEncoding/ConsoleEncoding.PInvoke.cs
  18. 2 0
      BepInEx.Core/ConsoleUtil/ConsoleEncoding/ConsoleEncoding.cs
  19. 16 38
      BepInEx.Core/ConsoleUtil/ConsoleWindow.cs
  20. 11 19
      BepInEx.Core/ConsoleUtil/Kon.cs
  21. 114 0
      BepInEx.Core/Console/Windows/WindowsConsoleDriver.cs
  22. 0 119
      BepInEx.Core/ConsoleManager.cs
  23. 6 2
      BepInEx.Core/Contract/Attributes.cs
  24. 26 0
      BepInEx.Core/Contract/PluginInfo.cs
  25. 8 8
      BepInEx.Core/Logging/ConsoleLogListener.cs
  26. 20 3
      BepInEx.Core/Logging/DiskLogListener.cs
  27. 46 0
      BepInEx.Core/Logging/HarmonyLogSource.cs
  28. 8 0
      BepInEx.Core/Logging/ILogListener.cs
  29. 9 0
      BepInEx.Core/Logging/ILogSource.cs
  30. 33 0
      BepInEx.Core/Logging/LogEventArgs.cs
  31. 3 0
      BepInEx.Core/Logging/LogLevel.cs
  32. 15 4
      BepInEx.Core/Logging/Logger.cs
  33. 45 0
      BepInEx.Core/Logging/ManualLogSource.cs
  34. 1 1
      BepInEx.Core/Logging/StdOutLogListener.cs
  35. 18 4
      BepInEx.Core/Logging/TraceLogSource.cs
  36. 7 1
      BepInEx.Core/Paths.cs
  37. 1 0
      BepInEx.Core/Properties/AssemblyInfo.cs
  38. 91 5
      BepInEx.Core/Utility.cs
  39. 2 2
      BepInEx.IL2CPP/BepInEx.IL2CPP.csproj
  40. 1 16
      BepInEx.IL2CPP/IL2CPPChainloader.cs
  41. 5 3
      BepInEx.IL2CPP/Preloader.cs
  42. 1 1
      BepInEx.NetLauncher/BepInEx.NetLauncher.csproj
  43. 0 3
      BepInEx.NetLauncher/NetPreloader.cs
  44. 1 1
      BepInEx.NetLauncher/Program.cs
  45. 7 6
      BepInEx.Preloader.Core/BepInEx.Preloader.Core.csproj
  46. 0 26
      BepInEx.Preloader.Core/Logging/BasicLogInfo.cs
  47. 48 0
      BepInEx.Preloader.Core/Logging/ChainloaderLogHelper.cs
  48. 34 0
      BepInEx.Preloader.Core/Logging/PreloaderConsoleListener.cs
  49. 0 90
      BepInEx.Preloader.Core/Logging/PreloaderLogWriter.cs
  50. 54 26
      BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs
  51. 2 0
      BepInEx.Preloader.Core/Patching/PatcherPlugin.cs
  52. 50 0
      BepInEx.Preloader.Core/RuntimeFixes/ConsoleSetOutFix.cs
  53. 0 36
      BepInEx.Preloader.Core/RuntimeFixes/HarmonyFixes.cs
  54. 2 1
      BepInEx.Preloader.Unity/BepInEx.Preloader.Unity.csproj
  55. 75 17
      BepInEx.Preloader.Unity/DoorstopEntrypoint.cs
  56. 6 0
      BepInEx.Preloader.Unity/EnvVars.cs
  57. 1 1
      BepInEx.Preloader.Unity/RuntimeFixes/TraceFix.cs
  58. 136 0
      BepInEx.Preloader.Unity/RuntimeFixes/XTermFix.cs
  59. 71 52
      BepInEx.Preloader.Unity/UnityPreloader.cs
  60. 1 1
      BepInEx.Unity/BepInEx.Unity.csproj
  61. 24 36
      BepInEx.Unity/Bootstrap/UnityChainloader.cs
  62. 14 4
      BepInEx.Unity/Logging/UnityLogListener.cs
  63. 10 3
      BepInEx.Unity/Logging/UnityLogSource.cs
  64. 1 1
      BepInEx.Unity/ThreadingHelper.cs
  65. 2 0
      CODE_OF_CONDUCT.md
  66. 4 3
      README.md

+ 6 - 6
.github/ISSUE_TEMPLATE/bug_report.md

@@ -5,21 +5,21 @@ about: Create a report to help us improve
 ---
 
 **Describe the bug**
-A clear and concise description of what the bug is.
+<!--- A clear and concise description of what the bug is. -->
 
 **To Reproduce**
-A short description of what you did for the bug to happen.
+<!--- A short description of what you did for the bug to happen. -->
 
 **Expected behavior**
-A clear and concise description of what you expected to happen.
+<!--- A clear and concise description of what you expected to happen. -->
 
 **Screenshots and logs**
-Include a console log (`output_log.txt` and/or `preloader_xxx_xxx.log`), if the error is visible there.  
-If not possible, include a screenshot of the error.
+<!--- Include a console log (`output_log.txt` and/or `preloader_xxx_xxx.log`), if the error is visible there. -->
+<!--- If not possible, include a screenshot of the error. -->
 
 **Desktop (please complete the following information):**
  - Game and game version [e.g. COM3D2 1.15]: 
  - BepInEx version [e.g. 4.0]: 
 
 **Additional context**
-Add any other context about the problem here.
+<!--- Add any other context about the problem here. -->

+ 4 - 4
.github/ISSUE_TEMPLATE/feature_request.md

@@ -5,13 +5,13 @@ about: Suggest an idea for this project
 ---
 
 **Is your feature request related to a problem? Please describe.**
-A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+<!--- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
 
 **Describe the solution you'd like**
-A clear and concise description of what you want to happen.
+<!--- A clear and concise description of what you want to happen. -->
 
 **Describe alternatives you've considered**
-A clear and concise description of any alternative solutions or features you've considered.
+<!--- A clear and concise description of any alternative solutions or features you've considered. -->
 
 **Additional context**
-Add any other context or screenshots about the feature request here.
+<!--- Add any other context or screenshots about the feature request here. -->

+ 28 - 0
.github/pull_request_template.md

@@ -0,0 +1,28 @@
+<!--- Provide a general summary of your changes in the Title above -->
+
+## Description
+<!--- Describe your changes in detail -->
+
+## Motivation and Context
+<!--- Why is this change required? What problem does it solve? -->
+<!--- If it fixes an open issue, please link to the issue here. -->
+
+## How Has This Been Tested?
+<!--- Please describe in detail how you tested your changes. -->
+<!--- Include details of your testing environment, tests ran to see how -->
+<!--- your change affects other areas of the code, etc. -->
+
+## Screenshots (if appropriate):
+
+## Types of changes
+<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
+- [ ] Bug fix (non-breaking change which fixes an issue)
+- [ ] New feature (non-breaking change which adds functionality)
+- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
+
+## Checklist:
+<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
+<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
+- [ ] My code follows the code style of this project.
+- [ ] My change requires a change to the documentation.
+- [ ] I have updated the documentation accordingly.

+ 26 - 7
BepInEx.Core/BepInEx.Core.csproj

@@ -39,6 +39,10 @@
     <DebugSymbols>true</DebugSymbols>
   </PropertyGroup>
   <ItemGroup>
+    <Reference Include="0Harmony, Version=2.1.0.0, Culture=neutral, processorArchitecture=MSIL">
+      <SpecificVersion>False</SpecificVersion>
+      <HintPath>C:\Users\Richard\.nuget\packages\harmonyx\2.1.0-beta.8\lib\net35\0Harmony.dll</HintPath>
+    </Reference>
     <Reference Include="System" />
   </ItemGroup>
   <ItemGroup>
@@ -47,7 +51,20 @@
     <Compile Include="Configuration\AcceptableValueList.cs" />
     <Compile Include="Configuration\AcceptableValueRange.cs" />
     <Compile Include="Configuration\ConfigEntryBase.cs" />
-    <Compile Include="ConsoleManager.cs" />
+    <Compile Include="Console\ConsoleManager.cs" />
+    <Compile Include="Console\IConsoleDriver.cs" />
+    <Compile Include="Console\SafeConsole.cs" />
+    <Compile Include="Console\Unix\ConsoleWriter.cs" />
+    <Compile Include="Console\Unix\LinuxConsoleDriver.cs" />
+    <Compile Include="Console\Unix\TtyHandler.cs" />
+    <Compile Include="Console\Unix\UnixStream.cs" />
+    <Compile Include="Console\Unix\UnixStreamHelper.cs" />
+    <Compile Include="Console\Windows\ConsoleEncoding\ConsoleEncoding.Buffers.cs" />
+    <Compile Include="Console\Windows\ConsoleEncoding\ConsoleEncoding.cs" />
+    <Compile Include="Console\Windows\ConsoleEncoding\ConsoleEncoding.PInvoke.cs" />
+    <Compile Include="Console\Windows\ConsoleWindow.cs" />
+    <Compile Include="Console\Windows\Kon.cs" />
+    <Compile Include="Console\Windows\WindowsConsoleDriver.cs" />
     <Compile Include="Contract\PluginInfo.cs" />
     <Compile Include="Configuration\ConfigDefinition.cs" />
     <Compile Include="Configuration\ConfigDescription.cs" />
@@ -57,13 +74,8 @@
     <Compile Include="Configuration\TomlTypeConverter.cs" />
     <Compile Include="Configuration\TypeConverter.cs" />
     <Compile Include="Contract\Attributes.cs" />
-    <Compile Include="ConsoleUtil\ConsoleEncoding\ConsoleEncoding.Buffers.cs" />
-    <Compile Include="ConsoleUtil\ConsoleEncoding\ConsoleEncoding.cs" />
-    <Compile Include="ConsoleUtil\ConsoleEncoding\ConsoleEncoding.PInvoke.cs" />
-    <Compile Include="ConsoleUtil\ConsoleWindow.cs" />
-    <Compile Include="ConsoleUtil\Kon.cs" />
-    <Compile Include="ConsoleUtil\SafeConsole.cs" />
     <Compile Include="Logging\DiskLogListener.cs" />
+    <Compile Include="Logging\HarmonyLogSource.cs" />
     <Compile Include="Logging\LogEventArgs.cs" />
     <Compile Include="Logging\Logger.cs" />
     <Compile Include="Logging\LogLevel.cs" />
@@ -79,9 +91,16 @@
     <Compile Include="Utility.cs" />
   </ItemGroup>
   <ItemGroup>
+    <PackageReference Include="HarmonyX">
+      <Version>2.1.0-beta.8</Version>
+    </PackageReference>
     <PackageReference Include="Mono.Cecil">
       <Version>0.10.4</Version>
     </PackageReference>
+    <PackageReference Include="MonoMod.Utils">
+      <Version>20.11.5.1</Version>
+    </PackageReference>
   </ItemGroup>
+  <ItemGroup />
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
 </Project>

+ 47 - 23
BepInEx.Core/Bootstrap/BaseChainloader.cs

@@ -15,12 +15,19 @@ namespace BepInEx.Bootstrap
 	{
 		#region Contract
 
-		protected virtual string ConsoleTitle => $"BepInEx {typeof(Paths).Assembly.GetName().Version} - {Process.GetCurrentProcess().ProcessName}";
+		protected virtual string ConsoleTitle => $"BepInEx {typeof(Paths).Assembly.GetName().Version} - {Paths.ProcessName}";
 
 		private bool _initialized = false;
 
+		/// <summary>
+		/// List of all <see cref="PluginInfo"/> instances loaded via the chainloader.
+		/// </summary>
 		public Dictionary<string, PluginInfo> Plugins { get; } = new Dictionary<string, PluginInfo>();
 
+		/// <summary>
+		/// Collection of error chainloader messages that occured during plugin loading.
+		/// Contains information about what certain plugins were not loaded.
+		/// </summary>
 		public List<string> DependencyErrors { get; } = new List<string>();
 
 		public virtual void Initialize(string gameExePath = null)
@@ -67,6 +74,9 @@ namespace BepInEx.Bootstrap
 
 			if (!TraceLogSource.IsListening)
 				Logger.Sources.Add(TraceLogSource.CreateSource());
+
+			if (!Logger.Sources.Any(x => x is HarmonyLogSource))
+				Logger.Sources.Add(new HarmonyLogSource());
 		}
 
 		protected virtual IList<PluginInfo> DiscoverPlugins()
@@ -84,27 +94,26 @@ namespace BepInEx.Bootstrap
 
 			foreach (var pluginInfoGroup in plugins.GroupBy(info => info.Metadata.GUID))
 			{
-				var alreadyLoaded = false;
+				PluginInfo loadedVersion = null;
 				foreach (var pluginInfo in pluginInfoGroup.OrderByDescending(x => x.Metadata.Version))
 				{
-					if (alreadyLoaded)
+					if (loadedVersion != null)
 					{
-						Logger.LogWarning($"Skipping because a newer version exists [{pluginInfo.Metadata.Name} {pluginInfo.Metadata.Version}]");
+						Logger.LogWarning($"Skipping [{pluginInfo}] because a newer version exists ({loadedVersion})");
 						continue;
 					}
 
-					alreadyLoaded = true;
-
 					// Perform checks that will prevent loading plugins in this run
 					var filters = pluginInfo.Processes.ToList();
 					bool invalidProcessName = filters.Count != 0 && filters.All(x => !string.Equals(x.ProcessName.Replace(".exe", ""), Paths.ProcessName, StringComparison.InvariantCultureIgnoreCase));
 
 					if (invalidProcessName)
 					{
-						Logger.LogWarning($"Skipping because of process filters [{pluginInfo.Metadata.Name} {pluginInfo.Metadata.Version}]");
+						Logger.LogWarning($"Skipping [{pluginInfo}] because of process filters ({string.Join(", ", pluginInfo.Processes.Select(p => p.ProcessName).ToArray())})");
 						continue;
 					}
 
+					loadedVersion = pluginInfo;
 					dependencyDict[pluginInfo.Metadata.GUID] = pluginInfo.Dependencies.Select(d => d.DependencyGUID);
 					pluginsByGuid[pluginInfo.Metadata.GUID] = pluginInfo;
 				}
@@ -118,13 +127,13 @@ namespace BepInEx.Bootstrap
 					dependencyDict.Remove(pluginInfo.Metadata.GUID);
 
 					var incompatiblePlugins = pluginInfo.Incompatibilities.Select(x => x.IncompatibilityGUID).Where(x => pluginsByGuid.ContainsKey(x)).ToArray();
-					string message = $@"Could not load [{pluginInfo.Metadata.Name}] because it is incompatible with: {string.Join(", ", incompatiblePlugins)}";
+					string message = $@"Could not load [{pluginInfo}] because it is incompatible with: {string.Join(", ", incompatiblePlugins)}";
 					DependencyErrors.Add(message);
 					Logger.LogError(message);
 				}
 				else if (PluginTargetsWrongBepin(pluginInfo))
 				{
-					string message = $@"Plugin [{pluginInfo.Metadata.Name}] targets a wrong version of BepInEx ({pluginInfo.TargettedBepInExVersion}) and might not work until you update";
+					string message = $@"Plugin [{pluginInfo}] targets a wrong version of BepInEx ({pluginInfo.TargettedBepInExVersion}) and might not work until you update";
 					DependencyErrors.Add(message);
 					Logger.LogWarning(message);
 				}
@@ -145,7 +154,7 @@ namespace BepInEx.Bootstrap
 			{
 				var plugins = DiscoverPlugins();
 
-				Logger.LogInfo($"{plugins.Count} plugins to load");
+				Logger.LogInfo($"{plugins.Count} plugin{(plugins.Count == 1 ? "" : "s")} to load");
 
 				var sortedPlugins = ModifyLoadOrder(plugins);
 
@@ -159,18 +168,21 @@ namespace BepInEx.Bootstrap
 					var missingDependencies = new List<BepInDependency>();
 					foreach (var dependency in plugin.Dependencies)
 					{
-						// If the depenency wasn't already processed, it's missing altogether
-						bool depenencyExists = processedPlugins.TryGetValue(dependency.DependencyGUID, out var pluginVersion);
-						if (!depenencyExists || pluginVersion < dependency.MinimumVersion)
+						bool IsHardDependency(BepInDependency dep)
+							=> (dep.Flags & BepInDependency.DependencyFlags.HardDependency) != 0;
+
+						// If the dependency wasn't already processed, it's missing altogether
+						bool dependencyExists = processedPlugins.TryGetValue(dependency.DependencyGUID, out var pluginVersion);
+						if (!dependencyExists || pluginVersion < dependency.MinimumVersion)
 						{
 							// If the dependency is hard, collect it into a list to show
-							if ((dependency.Flags & BepInDependency.DependencyFlags.HardDependency) != 0)
+							if (IsHardDependency(dependency))
 								missingDependencies.Add(dependency);
 							continue;
 						}
 
-						// If the dependency is invalid (e.g. has missing depedencies), report that to the user
-						if (invalidPlugins.Contains(dependency.DependencyGUID))
+						// If the dependency is a hard and is invalid (e.g. has missing dependencies), report that to the user
+						if (invalidPlugins.Contains(dependency.DependencyGUID) && IsHardDependency(dependency))
 						{
 							dependsOnInvalidPlugin = true;
 							break;
@@ -181,7 +193,7 @@ namespace BepInEx.Bootstrap
 
 					if (dependsOnInvalidPlugin)
 					{
-						string message = $"Skipping [{plugin.Metadata.Name}] because it has a dependency that was not loaded. See previous errors for details.";
+						string message = $"Skipping [{plugin}] because it has a dependency that was not loaded. See previous errors for details.";
 						DependencyErrors.Add(message);
 						Logger.LogWarning(message);
 						continue;
@@ -191,7 +203,7 @@ namespace BepInEx.Bootstrap
 					{
 						bool IsEmptyVersion(Version v) => v.Major == 0 && v.Minor == 0 && v.Build <= 0 && v.Revision <= 0;
 
-						string message = $@"Could not load [{plugin.Metadata.Name}] because it has missing dependencies: {
+						string message = $@"Could not load [{plugin}] because it has missing dependencies: {
 								string.Join(", ", missingDependencies.Select(s => IsEmptyVersion(s.MinimumVersion) ? s.DependencyGUID : $"{s.DependencyGUID} (v{s.MinimumVersion} or newer)").ToArray())
 							}";
 						DependencyErrors.Add(message);
@@ -203,7 +215,7 @@ namespace BepInEx.Bootstrap
 
 					try
 					{
-						Logger.LogInfo($"Loading [{plugin.Metadata.Name} {plugin.Metadata.Version}]");
+						Logger.LogInfo($"Loading [{plugin}]");
 
 						if (!loadedAssemblies.TryGetValue(plugin.Location, out var ass))
 							loadedAssemblies[plugin.Location] = ass = Assembly.LoadFile(plugin.Location);
@@ -218,7 +230,7 @@ namespace BepInEx.Bootstrap
 						invalidPlugins.Add(plugin.Metadata.GUID);
 						Plugins.Remove(plugin.Metadata.GUID);
 
-						Logger.LogError($"Error loading [{plugin.Metadata.Name}] : {ex.Message}");
+						Logger.LogError($"Error loading [{plugin}] : {ex.Message}");
 						if (ex is ReflectionTypeLoadException re)
 							Logger.LogDebug(TypeLoader.TypeLoadExceptionToString(re));
 						else
@@ -228,6 +240,12 @@ namespace BepInEx.Bootstrap
 			}
 			catch (Exception ex)
 			{
+				try
+				{
+					ConsoleManager.CreateConsole();
+				}
+				catch { }
+
 				Logger.LogError("Error occurred starting the game");
 				Logger.LogDebug(ex);
 			}
@@ -241,6 +259,12 @@ namespace BepInEx.Bootstrap
 
 		private static Regex allowedGuidRegex { get; } = new Regex(@"^[a-zA-Z0-9\._\-]+$");
 
+		/// <summary>
+		/// Analyzes the given type definition and attempts to convert it to a valid <see cref="PluginInfo"/>
+		/// </summary>
+		/// <param name="type">Type definition to analyze.</param>
+		/// <param name="assemblyLocation">The filepath of the assembly, to keep as metadata.</param>
+		/// <returns>If the type represent a valid plugin, returns a <see cref="PluginInfo"/> instance. Otherwise, return null.</returns>
 		public static PluginInfo ToPluginInfo(TypeDefinition type, string assemblyLocation)
 		{
 			if (type.IsInterface || type.IsAbstract)
@@ -338,9 +362,9 @@ namespace BepInEx.Bootstrap
 			"Enables writing log messages to disk.");
 
 		private static readonly ConfigEntry<LogLevel> ConfigDiskConsoleDisplayedLevel = ConfigFile.CoreConfig.Bind(
-			"Logging.Disk", "DisplayedLogLevel",
-			LogLevel.Info,
-			"Only displays the specified log level and above in the console output.");
+			"Logging.Disk", "LogLevels",
+			LogLevel.Fatal | LogLevel.Error | LogLevel.Warning | LogLevel.Message | LogLevel.Info,
+			"Only displays the specified log levels in the disk log output.");
 
 		#endregion
 	}

+ 22 - 7
BepInEx.Core/Bootstrap/TypeLoader.cs

@@ -51,36 +51,47 @@ namespace BepInEx.Bootstrap
 	/// </summary>
 	public static class TypeLoader
 	{
+		/// <summary>
+		/// Default assembly resolved used by the <see cref="TypeLoader"/>
+		/// </summary>
 		public static readonly DefaultAssemblyResolver CecilResolver;
-		private static readonly ReaderParameters readerParameters;
+
+		/// <summary>
+		/// Default reader parameters used by <see cref="TypeLoader"/>
+		/// </summary>
+		public static readonly ReaderParameters ReaderParameters;
 
 		public static HashSet<string> SearchDirectories = new HashSet<string>();
 
 		static TypeLoader()
 		{
 			CecilResolver = new DefaultAssemblyResolver();
-			readerParameters = new ReaderParameters { AssemblyResolver = CecilResolver };
+			ReaderParameters = new ReaderParameters { AssemblyResolver = CecilResolver };
 
 			CecilResolver.ResolveFailure += CecilResolveOnFailure;
 		}
 
 		public static AssemblyDefinition CecilResolveOnFailure(object sender, AssemblyNameReference reference)
 		{
-			var name = new AssemblyName(reference.FullName);
+			if (!Utility.TryParseAssemblyName(reference.FullName, out var name))
+				return null;
 
-			if (Utility.TryResolveDllAssembly(name, Paths.BepInExAssemblyDirectory, readerParameters, out var assembly) ||
-				Utility.TryResolveDllAssembly(name, Paths.PluginPath, readerParameters, out assembly))
+			if (Utility.TryResolveDllAssembly(name, Paths.BepInExAssemblyDirectory, ReaderParameters, out var assembly) ||
+				Utility.TryResolveDllAssembly(name, Paths.PluginPath, ReaderParameters, out assembly))
 				return assembly;
 
 			foreach (var dir in SearchDirectories)
 			{
-				if (Utility.TryResolveDllAssembly(name, Paths.BepInExAssemblyDirectory, readerParameters, out assembly))
+				if (Utility.TryResolveDllAssembly(name, dir, ReaderParameters, out assembly))
 					return assembly;
 			}
 
 			return AssemblyResolve?.Invoke(sender, reference);
 		}
 
+		/// <summary>
+		/// Event fired when <see cref="TypeLoader"/> fails to resolve a type during type loading.
+		/// </summary>
 		public static event AssemblyResolveEventHandler AssemblyResolve;
 
         /// <summary>
@@ -113,7 +124,7 @@ namespace BepInEx.Bootstrap
 						}
 					}
 
-					var ass = AssemblyDefinition.ReadAssembly(dll, readerParameters);
+					var ass = AssemblyDefinition.ReadAssembly(dll, ReaderParameters);
 
 					Logger.LogDebug($"Examining '{dll}'");
 
@@ -130,6 +141,10 @@ namespace BepInEx.Bootstrap
 					result[dll] = matches;
 					ass.Dispose();
 				}
+				catch (BadImageFormatException e)
+				{
+					Logger.LogDebug($"Skipping loading {dll} because it's not a valid .NET assembly. Full error: {e.Message}");
+				}
 				catch (Exception e)
 				{
 					Logger.LogError(e.ToString());

+ 4 - 4
BepInEx.Core/Configuration/ConfigFile.cs

@@ -118,7 +118,7 @@ namespace BepInEx.Configuration
 						continue;
 					}
 
-					string[] split = line.Split('='); //actual config line
+					string[] split = line.Split(new[] { '=' }, 2); //actual config line
 					if (split.Length != 2)
 						continue; //empty/invalid line
 
@@ -149,7 +149,7 @@ namespace BepInEx.Configuration
 				string directoryName = Path.GetDirectoryName(ConfigFilePath);
 				if (directoryName != null) Directory.CreateDirectory(directoryName);
 
-				using (var writer = new StreamWriter(File.Create(ConfigFilePath), Encoding.UTF8))
+				using (var writer = new StreamWriter(ConfigFilePath, false, Encoding.UTF8))
 				{
 					if (_ownerMetadata != null)
 					{
@@ -219,7 +219,7 @@ namespace BepInEx.Configuration
 		/// <summary>
 		/// Access one of the existing settings. If the setting has not been added yet, false is returned. Otherwise, true.
 		/// If the setting exists but has a different type than T, an exception is thrown.
-		/// New settings should be added with <see cref="Bind{T}"/>.
+		/// New settings should be added with <see cref="Bind{T}(BepInEx.Configuration.ConfigDefinition,T,BepInEx.Configuration.ConfigDescription)"/>.
 		/// </summary>
 		/// <typeparam name="T">Type of the value contained in this setting.</typeparam>
 		/// <param name="configDefinition">Section and Key of the setting.</param>
@@ -242,7 +242,7 @@ namespace BepInEx.Configuration
 		/// <summary>
 		/// Access one of the existing settings. If the setting has not been added yet, null is returned.
 		/// If the setting exists but has a different type than T, an exception is thrown.
-		/// New settings should be added with <see cref="Bind{T}"/>.
+		/// New settings should be added with <see cref="Bind{T}(BepInEx.Configuration.ConfigDefinition,T,BepInEx.Configuration.ConfigDescription)"/>.
 		/// </summary>
 		/// <typeparam name="T">Type of the value contained in this setting.</typeparam>
 		/// <param name="section">Section/category/group of the setting. Settings are grouped by this.</param>

+ 133 - 0
BepInEx.Core/Console/ConsoleManager.cs

@@ -0,0 +1,133 @@
+using System;
+using System.ComponentModel;
+using System.IO;
+using System.Text;
+using BepInEx.Configuration;
+using BepInEx.Unix;
+using MonoMod.Utils;
+
+namespace BepInEx
+{
+	public static class ConsoleManager
+	{
+		private const uint SHIFT_JIS_CP = 932;
+		
+		internal static IConsoleDriver Driver { get; set; }
+
+		/// <summary>
+		/// True if an external console has been started, false otherwise.
+		/// </summary>
+		public static bool ConsoleActive => Driver?.ConsoleActive ?? false;
+
+		/// <summary>
+		/// The stream that writes to the standard out stream of the process. Should never be null.
+		/// </summary>
+		public static TextWriter StandardOutStream => Driver?.StandardOut;
+
+		/// <summary>
+		/// The stream that writes to an external console. Null if no such console exists
+		/// </summary>
+		public static TextWriter ConsoleStream => Driver?.ConsoleOut;
+
+
+		public static void Initialize(bool alreadyActive)
+		{
+			switch (Utility.CurrentPlatform & ~Platform.Bits64)
+			{
+				case Platform.MacOS:
+				case Platform.Linux:
+				{
+					Driver = new LinuxConsoleDriver();
+					break;
+				}
+
+				case Platform.Windows:
+				{
+					Driver = new WindowsConsoleDriver();
+					break;
+				}
+
+				default:
+				{
+					throw new PlatformNotSupportedException("Was unable to determine console driver for platform " + Utility.CurrentPlatform);
+				}
+			}
+
+			Driver.Initialize(alreadyActive);
+		}
+
+		private static void DriverCheck()
+		{
+			if (Driver == null)
+				throw new InvalidOperationException("Driver has not been initialized");
+		}
+
+		public static void CreateConsole()
+		{
+			if (ConsoleActive)
+				return;
+
+			DriverCheck();
+			
+			// Apparently some versions of Mono throw a "Encoding name 'xxx' not supported"
+			// if you use Encoding.GetEncoding
+			// That's why we use of codepages directly and handle then in console drivers separately
+			var codepage = ConfigConsoleShiftJis.Value ? SHIFT_JIS_CP: (uint)Encoding.UTF8.CodePage;
+			
+			Driver.CreateConsole(codepage);
+			Console.SetOut(ConsoleStream);
+		}
+
+		public static void DetachConsole()
+		{
+			if (!ConsoleActive)
+				return;
+
+			DriverCheck();
+
+			Driver.DetachConsole();
+		}
+
+		public static void SetConsoleTitle(string title)
+		{
+			DriverCheck();
+
+			Driver.SetConsoleTitle(title);
+		}
+
+		public static void SetConsoleColor(ConsoleColor color)
+		{
+			DriverCheck();
+
+			Driver.SetConsoleColor(color);
+		}
+
+		public static readonly ConfigEntry<bool> ConfigConsoleEnabled = ConfigFile.CoreConfig.Bind(
+			"Logging.Console", "Enabled",
+			false,
+			"Enables showing a console for log output.");
+
+		public static readonly ConfigEntry<bool> ConfigConsoleShiftJis = ConfigFile.CoreConfig.Bind(
+			"Logging.Console", "ShiftJisEncoding",
+			false,
+			"If true, console is set to the Shift-JIS encoding, otherwise UTF-8 encoding.");
+
+		public static readonly ConfigEntry<ConsoleOutRedirectType> ConfigConsoleOutRedirectType = ConfigFile.CoreConfig.Bind(
+				"Logging.Console", "StandardOutType",
+				ConsoleOutRedirectType.Auto,
+				new StringBuilder()
+					.AppendLine("Hints console manager on what handle to assign as StandardOut. Possible values:")
+					.AppendLine("Auto - lets BepInEx decide how to redirect console output")
+					.AppendLine("ConsoleOut - prefer redirecting to console output; if possible, closes original standard output")
+					.AppendLine("StandardOut - prefer redirecting to standard output; if possible, closes console out")
+					.ToString()
+			);
+		
+		public enum ConsoleOutRedirectType
+		{
+			[Description("Auto")] Auto = 0,
+			[Description("Console Out")] ConsoleOut,
+			[Description("Standard Out")] StandardOut,
+		}
+	}
+}

+ 26 - 0
BepInEx.Core/Console/IConsoleDriver.cs

@@ -0,0 +1,26 @@
+using System;
+using System.IO;
+using System.Text;
+
+namespace BepInEx
+{
+	internal interface IConsoleDriver
+	{
+		TextWriter StandardOut { get; }
+		TextWriter ConsoleOut { get; }
+
+		bool ConsoleActive { get; }
+		bool ConsoleIsExternal { get; }
+
+		void Initialize(bool alreadyActive);
+		
+		// Apparently Windows code-pages work in Mono.
+		// https://stackoverflow.com/a/33456543
+		void CreateConsole(uint codepage);
+		void DetachConsole();
+
+		void SetConsoleColor(ConsoleColor color);
+		
+		void SetConsoleTitle(string title);
+	}
+}

+ 46 - 9
BepInEx.Core/ConsoleUtil/SafeConsole.cs

@@ -8,23 +8,42 @@ using System.Reflection;
 
 namespace UnityInjector.ConsoleUtil
 {
+	/// <summary>
+	/// Console class with safe handlers for Unity 4.x, which does not have a proper Console implementation
+	/// </summary>
 	internal static class SafeConsole
 	{
+		public static bool BackgroundColorExists { get; private set; }
+
 		private static GetColorDelegate _getBackgroundColor;
-		private static GetColorDelegate _getForegroundColor;
 		private static SetColorDelegate _setBackgroundColor;
-		private static SetColorDelegate _setForegroundColor;
 
 		public static ConsoleColor BackgroundColor
 		{
-			get { return _getBackgroundColor(); }
-			set { _setBackgroundColor(value); }
+			get => _getBackgroundColor();
+			set => _setBackgroundColor(value);
 		}
 
+		public static bool ForegroundColorExists { get; private set; }
+
+		private static GetColorDelegate _getForegroundColor;
+		private static SetColorDelegate _setForegroundColor;
+
 		public static ConsoleColor ForegroundColor
 		{
-			get { return _getForegroundColor(); }
-			set { _setForegroundColor(value); }
+			get => _getForegroundColor();
+			set => _setForegroundColor(value);
+		}
+
+		public static bool TitleExists { get; private set; }
+
+		private static GetStringDelegate _getTitle;
+		private static SetStringDelegate _setTitle;
+
+		public static string Title
+		{
+			get => _getTitle();
+			set => _setTitle(value);
 		}
 
 		static SafeConsole()
@@ -37,10 +56,14 @@ namespace UnityInjector.ConsoleUtil
 		{
 			const BindingFlags BINDING_FLAGS = BindingFlags.Public | BindingFlags.Static;
 
-			var sfc = tConsole.GetMethod("set_ForegroundColor", BINDING_FLAGS);
-			var sbc = tConsole.GetMethod("set_BackgroundColor", BINDING_FLAGS);
 			var gfc = tConsole.GetMethod("get_ForegroundColor", BINDING_FLAGS);
+			var sfc = tConsole.GetMethod("set_ForegroundColor", BINDING_FLAGS);
+
 			var gbc = tConsole.GetMethod("get_BackgroundColor", BINDING_FLAGS);
+			var sbc = tConsole.GetMethod("set_BackgroundColor", BINDING_FLAGS);
+			
+			var gtt = tConsole.GetMethod("get_Title", BINDING_FLAGS);
+			var stt = tConsole.GetMethod("set_Title", BINDING_FLAGS);
 
 			_setForegroundColor = sfc != null
 				? (SetColorDelegate)Delegate.CreateDelegate(typeof(SetColorDelegate), sfc)
@@ -57,10 +80,24 @@ namespace UnityInjector.ConsoleUtil
 			_getBackgroundColor = gbc != null
 				? (GetColorDelegate)Delegate.CreateDelegate(typeof(GetColorDelegate), gbc)
 				: (() => ConsoleColor.Black);
+
+			_getTitle = gtt != null
+				? (GetStringDelegate)Delegate.CreateDelegate(typeof(GetStringDelegate), gtt)
+				: (() => string.Empty);
+
+			_setTitle = stt != null
+				? (SetStringDelegate)Delegate.CreateDelegate(typeof(SetStringDelegate), stt)
+				: (value => { });
+
+			BackgroundColorExists = _setBackgroundColor != null && _getBackgroundColor != null;
+			ForegroundColorExists = _setForegroundColor != null && _getForegroundColor != null;
+			TitleExists = _setTitle != null && _getTitle != null;
 		}
 
 		private delegate ConsoleColor GetColorDelegate();
-
 		private delegate void SetColorDelegate(ConsoleColor value);
+
+		private delegate string GetStringDelegate();
+		private delegate void SetStringDelegate(string value);
 	}
 }

+ 57 - 0
BepInEx.Core/Console/Unix/ConsoleWriter.cs

@@ -0,0 +1,57 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using HarmonyLib;
+
+namespace BepInEx.Unix
+{
+	internal static class ConsoleWriter
+	{
+		private static Func<Stream, Encoding, bool, StreamWriter> cStreamWriterConstructor;
+		
+		private static Func<Stream, Encoding, bool, StreamWriter> CStreamWriterConstructor
+		{
+			get
+			{
+				if (cStreamWriterConstructor != null)
+					return cStreamWriterConstructor;
+
+				var cStreamWriter = AccessTools.TypeByName("System.IO.CStreamWriter");
+				Func<Stream, Encoding, bool, StreamWriter> GetCtor(int[] perm)
+				{
+					var parameters = new[] { typeof(Stream), typeof(Encoding), typeof(bool) };
+					var ctor = AccessTools.Constructor(cStreamWriter, perm.Select(i => parameters[i]).ToArray());
+					if (ctor != null)
+					{
+						return (stream, encoding, l) =>
+						{
+							var vals = new object[] { stream, encoding, l };
+							return (StreamWriter)ctor.Invoke(perm.Select(i => vals[i]).ToArray());
+						};
+					}
+					return null;
+				}
+
+				var ctorParams = new []
+				{
+					new[] { 0, 1, 2 }, // Unity 5.x and up
+					new[] { 0, 1 }     // Unity 4.7 and older
+				};
+				
+				cStreamWriterConstructor = ctorParams.Select(GetCtor).FirstOrDefault(f => f != null);
+				if (cStreamWriterConstructor == null)
+					throw new AmbiguousMatchException("Failed to find suitable constructor for CStreamWriter");
+				return cStreamWriterConstructor;
+			}
+		}
+
+		public static TextWriter CreateConsoleStreamWriter(Stream stream, Encoding encoding, bool leaveOpen)
+		{
+			var writer = CStreamWriterConstructor(stream, encoding, leaveOpen);
+			writer.AutoFlush = true;
+			return writer;
+		}
+	}
+}

+ 112 - 0
BepInEx.Core/Console/Unix/LinuxConsoleDriver.cs

@@ -0,0 +1,112 @@
+using System;
+using System.IO;
+using BepInEx.Logging;
+using HarmonyLib;
+using UnityInjector.ConsoleUtil;
+
+namespace BepInEx.Unix
+{
+	internal class LinuxConsoleDriver : IConsoleDriver
+	{
+		public static bool UseMonoTtyDriver { get; }
+
+		static LinuxConsoleDriver()
+		{
+			UseMonoTtyDriver = false;
+
+			var consoleDriverType = typeof(Console).Assembly.GetType("System.ConsoleDriver");
+
+			if (consoleDriverType != null)
+			{
+				UseMonoTtyDriver = typeof(Console).Assembly.GetType("System.ParameterizedStrings") != null;
+			}
+		}
+
+		public TextWriter StandardOut { get; private set; }
+		public TextWriter ConsoleOut { get; private set;  }
+
+		public bool ConsoleActive { get; private set; }
+		public bool ConsoleIsExternal => false;
+
+		public bool StdoutRedirected { get; private set; }
+
+		public TtyInfo TtyInfo { get; private set; }
+
+		public void Initialize(bool alreadyActive)
+		{
+			// Console is always considered active on Unix
+			ConsoleActive = true;
+
+			StdoutRedirected = UnixStreamHelper.isatty(1) != 1;
+
+			var duplicateStream = UnixStreamHelper.CreateDuplicateStream(1);
+
+			if (UseMonoTtyDriver && !StdoutRedirected)
+			{
+				// Mono implementation handles xterm for us
+
+				var writer = ConsoleWriter.CreateConsoleStreamWriter(duplicateStream, Console.Out.Encoding, true);
+
+				StandardOut = TextWriter.Synchronized(writer);
+
+				var driver = AccessTools.Field(AccessTools.TypeByName("System.ConsoleDriver"), "driver").GetValue(null);
+				AccessTools.Field(AccessTools.TypeByName("System.TermInfoDriver"), "stdout").SetValue(driver, writer);
+			}
+			else
+			{
+				// Handle TTY ourselves
+
+				var writer = new StreamWriter(duplicateStream, Console.Out.Encoding);
+
+				writer.AutoFlush = true;
+
+				StandardOut = TextWriter.Synchronized(writer);
+
+				TtyInfo = TtyHandler.GetTtyInfo();
+			}
+
+			ConsoleOut = StandardOut;
+		}
+
+		public void CreateConsole(uint codepage)
+		{
+			Logger.LogWarning("An external console currently cannot be spawned on a Unix platform.");
+		}
+
+		public void DetachConsole()
+		{
+			throw new PlatformNotSupportedException("Cannot detach console on a Unix platform");
+		}
+
+		public void SetConsoleColor(ConsoleColor color)
+		{
+			if (StdoutRedirected)
+				return;
+
+			if (UseMonoTtyDriver)
+			{
+				// Use mono's inbuilt terminfo driver to set the foreground color for us
+				SafeConsole.ForegroundColor = color;
+			}
+			else
+			{
+				ConsoleOut.Write(TtyInfo.GetAnsiCode(color));
+			}
+		}
+
+		public void SetConsoleTitle(string title)
+		{
+			if (StdoutRedirected)
+				return;
+
+			if (UseMonoTtyDriver && SafeConsole.TitleExists)
+			{
+				SafeConsole.Title = title;
+			}
+			else
+			{
+				ConsoleOut.Write($"\u001B]2;{title.Replace("\\", "\\\\")}\u0007");
+			}
+		}
+	}
+}

+ 214 - 0
BepInEx.Core/Console/Unix/TtyHandler.cs

@@ -0,0 +1,214 @@
+// Sections of this code have been abridged from https://github.com/mono/mono/blob/master/mcs/class/corlib/System/TermInfoReader.cs under the MIT license
+
+using System;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+namespace BepInEx.Unix
+{
+	internal class TtyInfo
+	{
+		public string TerminalType { get; set; } = "default";
+
+		public int MaxColors { get; set; }
+
+		public string[] ForegroundColorStrings { get; set; }
+
+		public static TtyInfo Default { get; } = new TtyInfo
+		{
+			MaxColors = 0
+		};
+
+		public string GetAnsiCode(ConsoleColor color)
+		{
+			if (MaxColors <= 0 || ForegroundColorStrings == null)
+				return string.Empty;
+
+			int index = (int)color % MaxColors;
+			return ForegroundColorStrings[index];
+		}
+	}
+
+	internal static class TtyHandler
+	{
+		private static readonly string[] ncursesLocations = new[]
+		{
+			"/usr/share/terminfo",
+			"/etc/terminfo",
+			"/usr/lib/terminfo",
+			"/lib/terminfo"
+		};
+
+		private static string TryTermInfoDir(string dir, string term)
+		{
+			string infoFilePath = $"{dir}/{(int)term[0]:x}/{term}";
+
+			if (File.Exists(infoFilePath))
+				return infoFilePath;
+
+			infoFilePath = Utility.CombinePaths(dir, term.Substring(0, 1), term);
+
+			if (File.Exists(infoFilePath))
+				return infoFilePath;
+
+			return null;
+		}
+
+		private static string FindTermInfoPath(string term)
+		{
+			if (string.IsNullOrEmpty(term))
+				return null;
+
+			string termInfoVar = Environment.GetEnvironmentVariable("TERMINFO");
+			if (termInfoVar != null && Directory.Exists(termInfoVar))
+			{
+				string text = TryTermInfoDir(termInfoVar, term);
+				if (text != null)
+				{
+					return text;
+				}
+			}
+
+			foreach (string location in ncursesLocations)
+			{
+				if (Directory.Exists(location))
+				{
+					string text = TryTermInfoDir(location, term);
+
+					if (text != null)
+						return text;
+				}
+			}
+
+			return null;
+		}
+
+		public static TtyInfo GetTtyInfo(string terminal = null)
+		{
+			terminal = terminal ?? Environment.GetEnvironmentVariable("TERM");
+			var path = FindTermInfoPath(terminal);
+
+			if (path == null)
+				return TtyInfo.Default;
+
+			byte[] buffer = File.ReadAllBytes(path);
+
+			var info = TtyInfoParser.Parse(buffer);
+			info.TerminalType = terminal;
+
+			return info;
+		}
+	}
+
+	internal static class TtyInfoParser
+	{
+		private static readonly int[] ansiColorMapping =
+		{
+			0, 4, 2, 6, 1, 5, 3, 7, 8, 12, 10, 14, 9, 13, 11, 15
+		};
+
+		public static TtyInfo Parse(byte[] buffer)
+		{
+			int intSize;
+
+
+			int magic = GetInt16(buffer, 0);
+
+			switch (magic)
+			{
+				case 0x11a:
+					intSize = 2;
+					break;
+
+				case 0x21E:
+					intSize = 4;
+					break;
+
+				default:
+					// Unknown ttyinfo format
+					return TtyInfo.Default;
+			}
+
+			int boolFieldLength = GetInt16(buffer, 4);
+			int intFieldLength = GetInt16(buffer, 6);
+			int strOffsetFieldLength = GetInt16(buffer, 8);
+
+			// Normally i'd put a more complete implementation here, but I only need to parse this info to get the max color count
+			// Feel free to implement the rest of this using these sources:
+			// https://github.com/mono/mono/blob/master/mcs/class/corlib/System/TermInfoReader.cs
+			// https://invisible-island.net/ncurses/man/term.5.html
+			// https://invisible-island.net/ncurses/man/terminfo.5.html
+
+			int baseOffset = 12 + GetString(buffer, 12).Length + 1; // Skip the terminal name
+			baseOffset += boolFieldLength; // Length of bool field section
+			baseOffset += baseOffset % 2; // Correct for boundary
+
+			int colorOffset =
+				baseOffset
+				+ (intSize * (int)TermInfoNumbers.MaxColors); // Finally the offset for the max color integer
+
+			//int stringOffset = baseOffset + (intSize * intFieldLength);
+
+			//int foregoundColorOffset =
+			//	stringOffset
+			//	+ (2 * (int)TermInfoStrings.SetAForeground);
+
+			//foregoundColorOffset = stringOffset
+			//					   + (2 * strOffsetFieldLength)
+			//					   + GetInt16(buffer, foregoundColorOffset);
+
+			var info = new TtyInfo();
+
+			info.MaxColors = GetInteger(intSize, buffer, colorOffset);
+
+			//string setForegroundTemplate = GetString(buffer, foregoundColorOffset);
+
+			//info.ForegroundColorStrings = ansiColorMapping.Select(x => setForegroundTemplate.Replace("%p1%", x.ToString())).ToArray();
+			info.ForegroundColorStrings = ansiColorMapping.Select(x => $"\u001B[{(x > 7 ? 82 + x : 30 + x)}m").ToArray();
+
+			return info;
+		}
+
+		private static int GetInt32(byte[] buffer, int offset)
+		{
+			return buffer[offset]
+				   | (buffer[offset + 1] << 8)
+				   | (buffer[offset + 2] << 16)
+				   | (buffer[offset + 3] << 24);
+		}
+
+		private static short GetInt16(byte[] buffer, int offset)
+		{
+			return (short)(buffer[offset]
+						   | (buffer[offset + 1] << 8));
+		}
+
+		private static int GetInteger(int intSize, byte[] buffer, int offset)
+		{
+			return intSize == 2
+				? GetInt16(buffer, offset)
+				: GetInt32(buffer, offset);
+		}
+
+		private static string GetString(byte[] buffer, int offset)
+		{
+			int length = 0;
+
+			while (buffer[offset + length] != 0x00)
+				length++;
+
+			return Encoding.ASCII.GetString(buffer, offset, length);
+		}
+
+		internal enum TermInfoNumbers
+		{
+			MaxColors = 13
+		}
+
+		internal enum TermInfoStrings
+		{
+			SetAForeground = 359
+		}
+	}
+}

+ 85 - 0
BepInEx.Core/Console/Unix/UnixStream.cs

@@ -0,0 +1,85 @@
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+
+namespace BepInEx.Unix
+{
+	internal class UnixStream : Stream
+	{
+		public override bool CanRead => Access == FileAccess.Read || Access == FileAccess.ReadWrite;
+		public override bool CanSeek => false;
+		public override bool CanWrite => Access == FileAccess.Write || Access == FileAccess.ReadWrite;
+		public override long Length => throw new InvalidOperationException();
+
+		public override long Position
+		{
+			get => throw new InvalidOperationException();
+			set => throw new InvalidOperationException();
+		}
+
+
+		public FileAccess Access { get; }
+
+		public IntPtr FileHandle { get; }
+
+		public UnixStream(int fileDescriptor, FileAccess access)
+		{
+			Access = access;
+
+			int newFd = UnixStreamHelper.dup(fileDescriptor);
+			FileHandle = UnixStreamHelper.fdopen(newFd, access == FileAccess.Write ? "w" : "r");
+		}
+
+
+		public override void Flush()
+		{
+			UnixStreamHelper.fflush(FileHandle);
+		}
+
+		public override long Seek(long offset, SeekOrigin origin)
+		{
+			throw new InvalidOperationException();
+		}
+
+		public override void SetLength(long value)
+		{
+			throw new InvalidOperationException();
+		}
+
+		public override int Read(byte[] buffer, int offset, int count)
+		{
+			GCHandle gcHandle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
+
+			var read = UnixStreamHelper.fread(new IntPtr(gcHandle.AddrOfPinnedObject().ToInt64() + offset), (IntPtr)count, (IntPtr)1, FileHandle);
+
+			gcHandle.Free();
+
+			return read.ToInt32();
+		}
+
+		public override void Write(byte[] buffer, int offset, int count)
+		{
+			GCHandle gcHandle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
+
+			UnixStreamHelper.fwrite(new IntPtr(gcHandle.AddrOfPinnedObject().ToInt64() + offset), (IntPtr)count, (IntPtr)1, FileHandle);
+
+			gcHandle.Free();
+		}
+
+		private void ReleaseUnmanagedResources()
+		{
+			UnixStreamHelper.fclose(FileHandle);
+		}
+
+		protected override void Dispose(bool disposing)
+		{
+			ReleaseUnmanagedResources();
+			base.Dispose(disposing);
+		}
+
+		~UnixStream()
+		{
+			Dispose(false);
+		}
+	}
+}

+ 60 - 0
BepInEx.Core/Console/Unix/UnixStreamHelper.cs

@@ -0,0 +1,60 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using MonoMod.Utils;
+
+namespace BepInEx.Unix
+{
+	internal static class UnixStreamHelper
+	{
+		public delegate int dupDelegate(int fd);
+		[DynDllImport("libc")]
+		public static dupDelegate dup;
+
+		public delegate IntPtr fdopenDelegate(int fd, string mode);
+		[DynDllImport("libc")]
+		public static fdopenDelegate fdopen;
+
+		public delegate IntPtr freadDelegate(IntPtr ptr, IntPtr size, IntPtr nmemb, IntPtr stream);
+		[DynDllImport("libc")]
+		public static freadDelegate fread;
+
+		public delegate int fwriteDelegate(IntPtr ptr, IntPtr size, IntPtr nmemb, IntPtr stream);
+		[DynDllImport("libc")]
+		public static fwriteDelegate fwrite;
+
+		public delegate int fcloseDelegate(IntPtr stream);
+		[DynDllImport("libc")]
+		public static fcloseDelegate fclose;
+
+		public delegate int fflushDelegate(IntPtr stream);
+		[DynDllImport("libc")]
+		public static fflushDelegate fflush;
+
+		public delegate int isattyDelegate(int fd);
+		[DynDllImport("libc")]
+		public static isattyDelegate isatty;
+
+		static UnixStreamHelper()
+		{
+			var libcMapping = new Dictionary<string, List<DynDllMapping>>
+			{
+				["libc"] = new List<DynDllMapping>
+				{
+					"libc.so.6", // Ubuntu glibc
+					"libc", // Linux glibc
+					"/usr/lib/libSystem.dylib", // OSX POSIX
+				}
+			};
+
+			typeof(UnixStreamHelper).ResolveDynDllImports(libcMapping);
+		}
+
+		public static Stream CreateDuplicateStream(int fileDescriptor)
+		{
+			int newFd = dup(fileDescriptor);
+
+			return new UnixStream(newFd, FileAccess.Write);
+		}
+	}
+}

BepInEx.Core/ConsoleUtil/ConsoleEncoding/ConsoleEncoding.Buffers.cs → BepInEx.Core/Console/Windows/ConsoleEncoding/ConsoleEncoding.Buffers.cs


BepInEx.Core/ConsoleUtil/ConsoleEncoding/ConsoleEncoding.PInvoke.cs → BepInEx.Core/Console/Windows/ConsoleEncoding/ConsoleEncoding.PInvoke.cs


+ 2 - 0
BepInEx.Core/ConsoleUtil/ConsoleEncoding/ConsoleEncoding.cs

@@ -20,6 +20,8 @@ namespace UnityInjector.ConsoleUtil
 		private readonly uint _codePage;
 		public override int CodePage => (int)_codePage;
 
+		public static Encoding OutputEncoding => new ConsoleEncoding(ConsoleCodePage);
+
 		public static uint ConsoleCodePage
 		{
 			get { return GetConsoleOutputCP(); }

+ 16 - 38
BepInEx.Core/ConsoleUtil/ConsoleWindow.cs

@@ -4,30 +4,24 @@
 // --------------------------------------------------
 
 using System;
-using System.IO;
 using System.Runtime.InteropServices;
-using System.Text;
-using Microsoft.Win32.SafeHandles;
+using BepInEx;
 
 namespace UnityInjector.ConsoleUtil
 {
 	internal class ConsoleWindow
 	{
 		public static bool IsAttached { get; private set; }
-		private static IntPtr _cOut;
-		private static IntPtr _oOut;
-
-		public static TextWriter OriginalOut { get; set; }
-
-		public static TextWriter StandardOut { get; private set; }
+		public static IntPtr ConsoleOutHandle;
+		public static IntPtr OriginalStdoutHandle;
 
 		public static void Attach()
 		{
 			if (IsAttached)
 				return;
 
-			if (_oOut == IntPtr.Zero)
-				_oOut = GetStdHandle(-11);
+			if (OriginalStdoutHandle == IntPtr.Zero)
+				OriginalStdoutHandle = GetStdHandle(-11);
 
 			// Store Current Window
 			IntPtr currWnd = GetForegroundWindow();
@@ -40,20 +34,14 @@ namespace UnityInjector.ConsoleUtil
 			// Restore Foreground
 			SetForegroundWindow(currWnd);
 
-			_cOut = CreateFile("CONOUT$", 0x80000000 | 0x40000000, 2, IntPtr.Zero, 3, 0, IntPtr.Zero);
-			BepInEx.ConsoleUtil.Kon.conOut = _cOut;
+			ConsoleOutHandle = CreateFile("CONOUT$", 0x80000000 | 0x40000000, 2, IntPtr.Zero, 3, 0, IntPtr.Zero);
+			BepInEx.ConsoleUtil.Kon.conOut = ConsoleOutHandle;
 
-			if (!SetStdHandle(-11, _cOut))
+			if (!SetStdHandle(-11, ConsoleOutHandle))
 				throw new Exception("SetStdHandle() failed");
 
-
-#warning Fix OriginalOut
-
-			//var originalOutStream = new FileStream(new SafeFileHandle(_oOut, false), FileAccess.Write);
-			//OriginalOut = new StreamWriter(originalOutStream, new UTF8Encoding(false));
-			OriginalOut = TextWriter.Null;
-
-			Init();
+			if (OriginalStdoutHandle != IntPtr.Zero && ConsoleManager.ConfigConsoleOutRedirectType.Value == ConsoleManager.ConsoleOutRedirectType.ConsoleOut)
+				CloseHandle(OriginalStdoutHandle);
 
 			IsAttached = true;
 		}
@@ -87,14 +75,16 @@ namespace UnityInjector.ConsoleUtil
 			if (!IsAttached)
 				return;
 
-			if (!CloseHandle(_cOut))
+			if (!CloseHandle(ConsoleOutHandle))
 				throw new Exception("CloseHandle() failed");
-			_cOut = IntPtr.Zero;
+
+			ConsoleOutHandle = IntPtr.Zero;
+
 			if (!FreeConsole())
 				throw new Exception("FreeConsole() failed");
-			if (!SetStdHandle(-11, _oOut))
+
+			if (!SetStdHandle(-11, OriginalStdoutHandle))
 				throw new Exception("SetStdHandle() failed");
-			Init();
 
 			IsAttached = false;
 		}
@@ -131,18 +121,6 @@ namespace UnityInjector.ConsoleUtil
 		[DllImport("kernel32.dll", SetLastError = true)]
 		private static extern IntPtr GetStdHandle(int nStdHandle);
 
-		private static void Init()
-		{
-			var stdOut = Console.OpenStandardOutput();
-			StandardOut = new StreamWriter(stdOut, Encoding.Default)
-			{
-				AutoFlush = true
-			};
-
-			Console.SetOut(StandardOut);
-			Console.SetError(StandardOut);
-		}
-
 		[DllImport("kernel32.dll", SetLastError = true)]
 		private static extern bool SetStdHandle(int nStdHandle, IntPtr hConsoleOutput);
 

+ 11 - 19
BepInEx.Core/ConsoleUtil/Kon.cs

@@ -64,27 +64,19 @@ namespace BepInEx.ConsoleUtil
 			succeeded = false;
 			if (!(conOut == INVALID_HANDLE_VALUE))
 			{
-				try
+				CONSOLE_SCREEN_BUFFER_INFO console_SCREEN_BUFFER_INFO;
+				if (!GetConsoleScreenBufferInfo(conOut, out console_SCREEN_BUFFER_INFO))
 				{
-					// FIXME: Windows console shouldn't be used in other OSs in the first place
-					CONSOLE_SCREEN_BUFFER_INFO console_SCREEN_BUFFER_INFO;
-					if (!GetConsoleScreenBufferInfo(conOut, out console_SCREEN_BUFFER_INFO))
-					{
-						bool consoleScreenBufferInfo = GetConsoleScreenBufferInfo(GetStdHandle(-12), out console_SCREEN_BUFFER_INFO);
-						if (!consoleScreenBufferInfo)
-							consoleScreenBufferInfo = GetConsoleScreenBufferInfo(GetStdHandle(-10), out console_SCREEN_BUFFER_INFO);
-						
-						if (!consoleScreenBufferInfo)
-							if (Marshal.GetLastWin32Error() == 6 && !throwOnNoConsole)
-								return default(CONSOLE_SCREEN_BUFFER_INFO);
-					}
-					succeeded = true;
-					return console_SCREEN_BUFFER_INFO;
-				}
-				catch (EntryPointNotFoundException)
-				{
-					// Fails under unsupported OSes
+					bool consoleScreenBufferInfo = GetConsoleScreenBufferInfo(GetStdHandle(-12), out console_SCREEN_BUFFER_INFO);
+					if (!consoleScreenBufferInfo)
+						consoleScreenBufferInfo = GetConsoleScreenBufferInfo(GetStdHandle(-10), out console_SCREEN_BUFFER_INFO);
+					
+					if (!consoleScreenBufferInfo)
+						if (Marshal.GetLastWin32Error() == 6 && !throwOnNoConsole)
+							return default(CONSOLE_SCREEN_BUFFER_INFO);
 				}
+				succeeded = true;
+				return console_SCREEN_BUFFER_INFO;
 			}
 
 			if (!throwOnNoConsole)

+ 114 - 0
BepInEx.Core/Console/Windows/WindowsConsoleDriver.cs

@@ -0,0 +1,114 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using BepInEx.ConsoleUtil;
+using HarmonyLib;
+using Microsoft.Win32.SafeHandles;
+using UnityInjector.ConsoleUtil;
+
+namespace BepInEx
+{
+	internal class WindowsConsoleDriver : IConsoleDriver
+	{
+		public TextWriter StandardOut { get; private set; }
+		public TextWriter ConsoleOut { get; private set; }
+
+		public bool ConsoleActive { get; private set; }
+		public bool ConsoleIsExternal => true;
+
+		public void Initialize(bool alreadyActive)
+		{
+			ConsoleActive = alreadyActive;
+
+			StandardOut = Console.Out;
+		}
+
+		// Apparently on some versions of Unity (e.g. 2018.4) using old mono causes crashes on game close if
+		// IntPtr overload is used for file streams (check #139).
+		// On the other hand, not all Unity games come with SafeFileHandle overload for FileStream
+		// As such, we're trying to use SafeFileHandle when it's available and go back to IntPtr overload if not available
+		private static readonly ConstructorInfo FileStreamCtor = new[]
+		{
+			AccessTools.Constructor(typeof(FileStream), new[] { typeof(SafeFileHandle), typeof(FileAccess) }),
+			AccessTools.Constructor(typeof(FileStream), new[] { typeof(IntPtr), typeof(FileAccess) }),
+		}.FirstOrDefault(m => m != null);
+
+		private static FileStream OpenFileStream(IntPtr handle)
+		{
+			var fileHandle = new SafeFileHandle(handle, false);
+			var ctorParams = AccessTools.ActualParameters(FileStreamCtor, new object[] { fileHandle, fileHandle.DangerousGetHandle(), FileAccess.Write });
+			return (FileStream)Activator.CreateInstance(typeof(FileStream), ctorParams);
+		}
+
+		public void CreateConsole(uint codepage)
+		{
+			ConsoleWindow.Attach();
+
+			// Make sure of ConsoleEncoding helper class because on some Monos
+			// Encoding.GetEncoding throws NotImplementedException on most codepages
+			// NOTE: We don't set Console.OutputEncoding because it resets any existing Console.Out writers
+			ConsoleEncoding.ConsoleCodePage = codepage;
+
+			// If stdout exists, write to it, otherwise make it the same as console out
+			// Not sure if this is needed? Does the original Console.Out still work?
+			var stdout = GetOutHandle();
+			if (stdout == IntPtr.Zero)
+			{
+				StandardOut = TextWriter.Null;
+				ConsoleOut = TextWriter.Null;
+				return;
+			}
+
+			var originalOutStream = OpenFileStream(stdout);
+			StandardOut = new StreamWriter(originalOutStream, new UTF8Encoding(false))
+			{
+				AutoFlush = true
+			};
+
+			var consoleOutStream = OpenFileStream(ConsoleWindow.ConsoleOutHandle);
+			// Can't use Console.OutputEncoding because it can be null (i.e. not preference by user)
+			ConsoleOut = new StreamWriter(consoleOutStream, ConsoleEncoding.OutputEncoding)
+			{
+				AutoFlush = true
+			};
+			ConsoleActive = true;
+		}
+
+		private IntPtr GetOutHandle()
+		{
+			switch (ConsoleManager.ConfigConsoleOutRedirectType.Value)
+			{
+				case ConsoleManager.ConsoleOutRedirectType.ConsoleOut:
+					return ConsoleWindow.ConsoleOutHandle;
+				case ConsoleManager.ConsoleOutRedirectType.StandardOut:
+					return ConsoleWindow.OriginalStdoutHandle;
+				case ConsoleManager.ConsoleOutRedirectType.Auto:
+				default:
+					return ConsoleWindow.OriginalStdoutHandle != IntPtr.Zero ? ConsoleWindow.OriginalStdoutHandle : ConsoleWindow.ConsoleOutHandle;
+			}
+		}
+
+		public void DetachConsole()
+		{
+			ConsoleWindow.Detach();
+
+			ConsoleOut.Close();
+			ConsoleOut = null;
+
+			ConsoleActive = false;
+		}
+
+		public void SetConsoleColor(ConsoleColor color)
+		{
+			SafeConsole.ForegroundColor = color;
+			Kon.ForegroundColor = color;
+		}
+
+		public void SetConsoleTitle(string title)
+		{
+			ConsoleWindow.Title = title;
+		}
+	}
+}

+ 0 - 119
BepInEx.Core/ConsoleManager.cs

@@ -1,119 +0,0 @@
-using System;
-using System.IO;
-using System.Text;
-using BepInEx.Configuration;
-using BepInEx.ConsoleUtil;
-using UnityInjector.ConsoleUtil;
-
-namespace BepInEx
-{
-	public static class ConsoleManager
-	{
-		public static bool ConsoleActive { get; private set; }
-
-		public static TextWriter StandardOut => ConsoleWindow.StandardOut;
-
-		public static void CreateConsole()
-		{
-			if (ConsoleActive)
-				return;
-
-			switch (Environment.OSVersion.Platform)
-			{
-				case PlatformID.Win32NT:
-				{
-					ConsoleWindow.Attach();
-					break;
-				}
-				default:
-					throw new PlatformNotSupportedException("Spawning a console is not currently supported on this platform");
-			}
-
-			ConsoleActive = true;
-		}
-
-		public static void DetachConsole()
-		{
-			if (!ConsoleActive)
-				return;
-
-			switch (Environment.OSVersion.Platform)
-			{
-				case PlatformID.Win32NT:
-				{
-					ConsoleWindow.Detach();
-					break;
-				}
-				default:
-					throw new PlatformNotSupportedException("Spawning a console is not currently supported on this platform");
-			}
-
-			ConsoleActive = false;
-		}
-
-		public static void SetConsoleEncoding(uint encodingCodePage)
-		{
-			if (!ConsoleActive)
-				throw new InvalidOperationException("Console is not currently active");
-
-			switch (Environment.OSVersion.Platform)
-			{
-				case PlatformID.Win32NT:
-				{
-					ConsoleEncoding.ConsoleCodePage = encodingCodePage;
-					Console.OutputEncoding = Encoding.GetEncoding((int)encodingCodePage);
-					break;
-				}
-				default:
-					throw new PlatformNotSupportedException("Spawning a console is not currently supported on this platform");
-			}
-		}
-
-		public static void SetConsoleTitle(string title)
-		{
-			switch (Environment.OSVersion.Platform)
-			{
-				case PlatformID.Win32NT:
-				{
-					if (!ConsoleActive)
-						return;
-
-					ConsoleWindow.Title = title;
-					break;
-				}
-			}
-		}
-
-		public static void SetConsoleColor(ConsoleColor color)
-		{
-			switch (Environment.OSVersion.Platform)
-			{
-				case PlatformID.Win32NT:
-				{
-					if (!ConsoleActive)
-						return;
-
-					Kon.ForegroundColor = color;
-					break;
-				}
-			}
-
-			Console.ForegroundColor = color;
-		}
-
-		public static void ForceSetActive(bool value)
-		{
-			ConsoleActive = value;
-		}
-
-		public static readonly ConfigEntry<bool> ConfigConsoleEnabled = ConfigFile.CoreConfig.Bind(
-			"Logging.Console", "Enabled",
-			false,
-			"Enables showing a console for log output.");
-
-		public static readonly ConfigEntry<bool> ConfigConsoleShiftJis = ConfigFile.CoreConfig.Bind(
-			"Logging.Console", "ShiftJisEncoding",
-			false,
-			"If true, console is set to the Shift-JIS encoding, otherwise UTF-8 encoding.");
-	}
-}

+ 6 - 2
BepInEx.Core/Contract/Attributes.cs

@@ -67,6 +67,10 @@ namespace BepInEx
 	[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
 	public class BepInDependency : Attribute, ICacheable
 	{
+		/// <summary>
+		/// Flags that are applied to a dependency
+		/// </summary>
+		[Flags]
 		public enum DependencyFlags
 		{
 			/// <summary>
@@ -110,7 +114,7 @@ namespace BepInEx
 
 		/// <summary>
 		/// Marks this <see cref="BaseUnityPlugin"/> as depenant on another plugin. The other plugin will be loaded before this one.
-		/// If the other plugin doesn't exist or is of a version below <see cref="MinimumDependencyVersion"/>, this plugin will not load and an error will be logged instead.
+		/// If the other plugin doesn't exist or is of a version below <see cref="MinimumVersion"/>, this plugin will not load and an error will be logged instead.
 		/// </summary>
 		/// <param name="DependencyGUID">The GUID of the referenced plugin.</param>
 		/// <param name="MinimumDependencyVersion">The minimum version of the referenced plugin.</param>
@@ -284,7 +288,7 @@ namespace BepInEx
 		/// <summary>
 		/// Retrieves the dependencies of the specified plugin type.
 		/// </summary>
-		/// <param name="Plugin">The plugin type.</param>
+		/// <param name="plugin">The plugin type.</param>
 		/// <returns>A list of all plugin types that the specified plugin type depends upon.</returns>
 		public static IEnumerable<BepInDependency> GetDependencies(Type plugin)
 		{

+ 26 - 0
BepInEx.Core/Contract/PluginInfo.cs

@@ -6,18 +6,41 @@ using BepInEx.Bootstrap;
 
 namespace BepInEx
 {
+	/// <summary>
+	/// Data class that represents information about a loadable BepInEx plugin.
+	/// Contains all metadata and additional info required for plugin loading by <see cref="Chainloader"/>.
+	/// </summary>
 	public class PluginInfo : ICacheable
 	{
+		/// <summary>
+		/// General metadata about a plugin.
+		/// </summary>
 		public BepInPlugin Metadata { get; internal set; }
 
+		/// <summary>
+		/// Collection of <see cref="BepInProcess"/> attributes that describe what processes the plugin can run on.
+		/// </summary>
 		public IEnumerable<BepInProcess> Processes { get; internal set; }
 
+		/// <summary>
+		/// Collection of <see cref="BepInDependency"/> attributes that describe what plugins this plugin depends on.
+		/// </summary>
 		public IEnumerable<BepInDependency> Dependencies { get; internal set; }
 
+		/// <summary>
+		/// Collection of <see cref="BepInIncompatibility"/> attributes that describe what plugins this plugin
+		/// is incompatible with.
+		/// </summary>
 		public IEnumerable<BepInIncompatibility> Incompatibilities { get; internal set; }
 
+		/// <summary>
+		/// File path to the plugin DLL
+		/// </summary>
 		public string Location { get; internal set; }
 
+		/// <summary>
+		/// Instance of the plugin that represents this info. NULL if no plugin is instantiated from info (yet)
+		/// </summary>
 		public object Instance { get; internal set; }
 
 		public string TypeName { get; internal set; }
@@ -88,5 +111,8 @@ namespace BepInEx
 
 			TargettedBepInExVersion = new Version(br.ReadString());
 		}
+
+		/// <inheritdoc />
+		public override string ToString() => $"{Metadata?.Name} {Metadata?.Version}";
 	}
 }

+ 8 - 8
BepInEx.Core/Logging/ConsoleLogListener.cs

@@ -8,23 +8,23 @@ namespace BepInEx.Logging
 	/// </summary>
 	public class ConsoleLogListener : ILogListener
 	{
+		/// <inheritdoc />
 		public void LogEvent(object sender, LogEventArgs eventArgs)
 		{
-			if (eventArgs.Level.GetHighestLevel() > ConfigConsoleDisplayedLevel.Value)
+			if ((eventArgs.Level & ConfigConsoleDisplayedLevel.Value) == 0)
 				return;
 
-			string log = $"[{eventArgs.Level,-7}:{((ILogSource)sender).SourceName,10}] {eventArgs.Data}\r\n";
-
 			ConsoleManager.SetConsoleColor(eventArgs.Level.GetConsoleColor());
-			Console.Write(log);
+			Console.Write(eventArgs.ToStringLine());
 			ConsoleManager.SetConsoleColor(ConsoleColor.Gray);
 		}
 
+		/// <inheritdoc />
 		public void Dispose() { }
 
-		private static readonly ConfigEntry<LogLevel> ConfigConsoleDisplayedLevel = ConfigFile.CoreConfig.Bind(
-			"Logging.Console","DisplayedLogLevel",
-			LogLevel.Info,
-			"Only displays the specified log level and above in the console output.");
+		protected static readonly ConfigEntry<LogLevel> ConfigConsoleDisplayedLevel = ConfigFile.CoreConfig.Bind(
+			"Logging.Console", "LogLevels",
+			LogLevel.Fatal | LogLevel.Error | LogLevel.Warning | LogLevel.Message | LogLevel.Info,
+			"Only displays the specified log levels in the console output.");
 	}
 }

+ 20 - 3
BepInEx.Core/Logging/DiskLogListener.cs

@@ -10,12 +10,27 @@ namespace BepInEx.Logging
 	/// </summary>
 	public class DiskLogListener : ILogListener
 	{
+		/// <summary>
+		/// Log levels to display.
+		/// </summary>
 		public LogLevel DisplayedLogLevel { get; set; }
 
+		/// <summary>
+		/// Writer for the disk log.
+		/// </summary>
 		public TextWriter LogWriter { get; protected set; }
 
+		/// <summary>
+		/// Timer for flushing the logs to a file.
+		/// </summary>
 		public Timer FlushTimer { get; protected set; }
 
+		/// <summary>
+		/// Creates a new disk log listener.
+		/// </summary>
+		/// <param name="localPath">Path to the log.</param>
+		/// <param name="displayedLogLevel">Log levels to display.</param>
+		/// <param name="appendLog">Whether to append logs to an already existing log file.</param>
 		public DiskLogListener(string localPath, LogLevel displayedLogLevel = LogLevel.Info, bool appendLog = false)
 		{
 			DisplayedLogLevel = displayedLogLevel;
@@ -24,7 +39,7 @@ namespace BepInEx.Logging
 
 			FileStream fileStream;
 
-			while (!Utility.TryOpenFileStream(Path.Combine(Paths.BepInExRootPath, localPath), appendLog ? FileMode.Append : FileMode.Create, out fileStream, share: FileShare.Read))
+			while (!Utility.TryOpenFileStream(Path.Combine(Paths.BepInExRootPath, localPath), appendLog ? FileMode.Append : FileMode.Create, out fileStream, share: FileShare.Read, access: FileAccess.Write))
 			{
 				if (counter == 5)
 				{
@@ -46,17 +61,19 @@ namespace BepInEx.Logging
 
 		public static HashSet<string> BlacklistedSources = new HashSet<string>();
 
+		/// <inheritdoc />
 		public void LogEvent(object sender, LogEventArgs eventArgs)
 		{
 			if (BlacklistedSources.Contains(eventArgs.Source.SourceName))
 				return;
 
-			if (eventArgs.Level.GetHighestLevel() > DisplayedLogLevel)
+			if ((eventArgs.Level & DisplayedLogLevel) == 0)
 				return;
 
-			LogWriter.WriteLine($"[{eventArgs.Level,-7}:{((ILogSource)sender).SourceName,10}] {eventArgs.Data}");
+			LogWriter.WriteLine(eventArgs.ToString());
 		}
 
+		/// <inheritdoc />
 		public void Dispose()
 		{
 			FlushTimer?.Dispose();

+ 46 - 0
BepInEx.Core/Logging/HarmonyLogSource.cs

@@ -0,0 +1,46 @@
+using System;
+using System.Collections.Generic;
+using BepInEx.Configuration;
+using HarmonyLogger = HarmonyLib.Tools.Logger;
+
+namespace BepInEx.Logging
+{
+	public class HarmonyLogSource : ILogSource
+	{
+		private static readonly ConfigEntry<HarmonyLogger.LogChannel> LogChannels = ConfigFile.CoreConfig.Bind(
+			"Harmony.Logger",
+			"LogChannels",
+			HarmonyLogger.LogChannel.Warn | HarmonyLogger.LogChannel.Error,
+			"Specifies which Harmony log channels to listen to.\nNOTE: IL channel dumps the whole patch methods, use only when needed!");
+
+		private static readonly Dictionary<HarmonyLogger.LogChannel, LogLevel> LevelMap = new Dictionary<HarmonyLogger.LogChannel, LogLevel>
+		{
+			[HarmonyLogger.LogChannel.Info] = LogLevel.Info,
+			[HarmonyLogger.LogChannel.Warn] = LogLevel.Warning,
+			[HarmonyLogger.LogChannel.Error] = LogLevel.Error,
+			[HarmonyLogger.LogChannel.IL] = LogLevel.Debug
+		};
+
+		public HarmonyLogSource()
+		{
+			HarmonyLogger.ChannelFilter = LogChannels.Value;
+			HarmonyLogger.MessageReceived += HandleHarmonyMessage;
+		}
+
+		private void HandleHarmonyMessage(object sender, HarmonyLib.Tools.Logger.LogEventArgs e)
+		{
+			if (!LevelMap.TryGetValue(e.LogChannel, out var level))
+				return;
+
+			LogEvent?.Invoke(this, new LogEventArgs(e.Message, level, this));
+		}
+
+		public void Dispose()
+		{
+			HarmonyLogger.MessageReceived -= HandleHarmonyMessage;
+		}
+
+		public string SourceName { get; } = "HarmonyX";
+		public event EventHandler<LogEventArgs> LogEvent;
+	}
+}

+ 8 - 0
BepInEx.Core/Logging/ILogListener.cs

@@ -2,8 +2,16 @@
 
 namespace BepInEx.Logging
 {
+	/// <summary>
+	/// A generic log listener that receives log events and can route them to some output (e.g. file, console, socket).
+	/// </summary>
 	public interface ILogListener : IDisposable
 	{
+		/// <summary>
+		/// Handle an incoming log event.
+		/// </summary>
+		/// <param name="sender">Log source that sent the event. Don't use; instead use <see cref="LogEventArgs.Source"/></param>
+		/// <param name="eventArgs">Information about the log message.</param>
 		void LogEvent(object sender, LogEventArgs eventArgs);
 	}
 }

+ 9 - 0
BepInEx.Core/Logging/ILogSource.cs

@@ -2,10 +2,19 @@
 
 namespace BepInEx.Logging
 {
+	/// <summary>
+	/// Log source that can output log messages.
+	/// </summary>
 	public interface ILogSource : IDisposable
 	{
+		/// <summary>
+		/// Name of the log source.
+		/// </summary>
 		string SourceName { get; }
 
+		/// <summary>
+		/// Event that sends the log message. Call <see cref="EventHandler.Invoke"/> to send a log message.
+		/// </summary>
 		event EventHandler<LogEventArgs> LogEvent;
 	}
 }

+ 33 - 0
BepInEx.Core/Logging/LogEventArgs.cs

@@ -2,19 +2,52 @@
 
 namespace BepInEx.Logging
 {
+	/// <summary>
+	/// Log event arguments. Contains info about the log message.
+	/// </summary>
 	public class LogEventArgs : EventArgs
 	{
+		/// <summary>
+		/// Logged data.
+		/// </summary>
 		public object Data { get; protected set; }
 
+		/// <summary>
+		/// Log levels for the data.
+		/// </summary>
 		public LogLevel Level { get; protected set; }
 
+		/// <summary>
+		/// Log source that emitted the log event.
+		/// </summary>
 		public ILogSource Source { get; protected set; }
 
+		/// <summary>
+		/// Creates the log event args-
+		/// </summary>
+		/// <param name="data">Logged data.</param>
+		/// <param name="level">Log level of the data.</param>
+		/// <param name="source">Log source that emits these args.</param>
 		public LogEventArgs(object data, LogLevel level, ILogSource source)
 		{
 			Data = data;
 			Level = level;
 			Source = source;
 		}
+
+		/// <inheritdoc />
+		public override string ToString()
+		{
+			return $"[{Level,-7}:{Source.SourceName,10}] {Data}";
+		}
+
+		/// <summary>
+		/// Like <see cref="ToString"/> but appends newline at the end.
+		/// </summary>
+		/// <returns>Same output as <see cref="ToString"/> but with new line.</returns>
+		public string ToStringLine()
+		{
+			return $"[{Level,-7}:{Source.SourceName,10}] {Data}{Environment.NewLine}";
+		}
 	}
 }

+ 3 - 0
BepInEx.Core/Logging/LogLevel.cs

@@ -49,6 +49,9 @@ namespace BepInEx.Logging
 		All = Fatal | Error | Warning | Message | Info | Debug
 	}
 
+	/// <summary>
+	/// Helper methods for log level handling.
+	/// </summary>
 	public static class LogLevelExtensions
 	{
 		/// <summary>

+ 15 - 4
BepInEx.Core/Logging/Logger.cs

@@ -4,17 +4,23 @@ using System.Collections.Generic;
 namespace BepInEx.Logging
 {
 	/// <summary>
-	/// A static <see cref="BaseLogger"/> instance.
+	/// Handles pub-sub event marshalling across all log listeners and sources.
 	/// </summary>
 	public static class Logger
 	{
+		/// <summary>
+		/// Collection of all log listeners that receive log events.
+		/// </summary>
 		public static ICollection<ILogListener> Listeners { get; } = new List<ILogListener>();
 
+		/// <summary>
+		/// Collection of all log source that output log events.
+		/// </summary>
 		public static ICollection<ILogSource> Sources { get; } = new LogSourceCollection();
 
 		private static readonly ManualLogSource InternalLogSource = CreateLogSource("BepInEx");
 
-		private static void InternalLogEvent(object sender, LogEventArgs eventArgs)
+		internal static void InternalLogEvent(object sender, LogEventArgs eventArgs)
 		{
 			foreach (var listener in Listeners)
 			{
@@ -23,10 +29,10 @@ namespace BepInEx.Logging
 		}
 
 		/// <summary>
-		/// Logs an entry to the current logger instance.
+		/// Logs an entry to the internal logger instance.
 		/// </summary>
 		/// <param name="level">The level of the entry.</param>
-		/// <param name="entry">The textual value of the entry.</param>
+		/// <param name="data">The data of the entry.</param>
 		internal static void Log(LogLevel level, object data)
 		{
 			InternalLogSource.Log(level, data);
@@ -39,6 +45,11 @@ namespace BepInEx.Logging
 		internal static void LogInfo(object data) => Log(LogLevel.Info, data);
 		internal static void LogDebug(object data) => Log(LogLevel.Debug, data);
 
+		/// <summary>
+		/// Creates a new log source with a name and attaches it to <see cref="Sources"/>.
+		/// </summary>
+		/// <param name="sourceName">Name of the log source to create.</param>
+		/// <returns>An instance of <see cref="ManualLogSource"/> that allows to write logs.</returns>
 		public static ManualLogSource CreateLogSource(string sourceName)
 		{
 			var source = new ManualLogSource(sourceName);

+ 45 - 0
BepInEx.Core/Logging/ManualLogSource.cs

@@ -2,28 +2,73 @@
 
 namespace BepInEx.Logging
 {
+	/// <summary>
+	/// A generic, multi-purpose log source. Exposes simple API to manually emit logs.
+	/// </summary>
 	public class ManualLogSource : ILogSource
 	{
+		/// <inheritdoc />
 		public string SourceName { get; }
+
+		/// <inheritdoc />
 		public event EventHandler<LogEventArgs> LogEvent;
 
+		/// <summary>
+		/// Creates a manual log source.
+		/// </summary>
+		/// <param name="sourceName">Name of the log source.</param>
 		public ManualLogSource(string sourceName)
 		{
 			SourceName = sourceName;
 		}
 
+		/// <summary>
+		/// Logs a message with the specified log level.
+		/// </summary>
+		/// <param name="level">Log levels to attach to the message. Multiple can be used with bitwise ORing.</param>
+		/// <param name="data">Data to log.</param>
 		public void Log(LogLevel level, object data)
 		{
 			LogEvent?.Invoke(this, new LogEventArgs(data, level, this));
 		}
 
+		/// <summary>
+		/// Logs a message with <see cref="LogLevel.Fatal"/> level.
+		/// </summary>
+		/// <param name="data">Data to log.</param>
 		public void LogFatal(object data) => Log(LogLevel.Fatal, data);
+
+		/// <summary>
+		/// Logs a message with <see cref="LogLevel.Error"/> level.
+		/// </summary>
+		/// <param name="data">Data to log.</param>
 		public void LogError(object data) => Log(LogLevel.Error, data);
+
+		/// <summary>
+		/// Logs a message with <see cref="LogLevel.Warning"/> level.
+		/// </summary>
+		/// <param name="data">Data to log.</param>
 		public void LogWarning(object data) => Log(LogLevel.Warning, data);
+
+		/// <summary>
+		/// Logs a message with <see cref="LogLevel.Message"/> level.
+		/// </summary>
+		/// <param name="data">Data to log.</param>
 		public void LogMessage(object data) => Log(LogLevel.Message, data);
+
+		/// <summary>
+		/// Logs a message with <see cref="LogLevel.Info"/> level.
+		/// </summary>
+		/// <param name="data">Data to log.</param>
 		public void LogInfo(object data) => Log(LogLevel.Info, data);
+
+		/// <summary>
+		/// Logs a message with <see cref="LogLevel.Debug"/> level.
+		/// </summary>
+		/// <param name="data">Data to log.</param>
 		public void LogDebug(object data) => Log(LogLevel.Debug, data);
 
+		/// <inheritdoc />
 		public void Dispose() { }
 	}
 }

+ 1 - 1
BepInEx.Core/Logging/StdOutLogListener.cs

@@ -12,7 +12,7 @@ namespace BepInEx.Core.Logging
 		{
 			string log = $"[{eventArgs.Level,-7}:{((ILogSource)sender).SourceName,10}] {eventArgs.Data}\r\n";
 
-			ConsoleWindow.OriginalOut?.Write(log);
+			ConsoleManager.StandardOutStream?.Write(log);
 		}
 
 		public void Dispose() { }

+ 18 - 4
BepInEx.Core/Logging/TraceLogSource.cs

@@ -3,15 +3,22 @@
 namespace BepInEx.Logging
 {
 	/// <summary>
-	/// A trace listener that writes to an underlying <see cref="BaseLogger"/> instance.
+	/// A source that routes all logs from the inbuilt .NET <see cref="Trace"/> API to the BepInEx logging system.
 	/// </summary>
 	/// <inheritdoc cref="TraceListener"/>
 	public class TraceLogSource : TraceListener
 	{
+		/// <summary>
+		/// Whether Trace logs are currently being rerouted.
+		/// </summary>
 		public static bool IsListening { get; protected set; } = false;
 
 		private static TraceLogSource traceListener;
 
+		/// <summary>
+		/// Creates a new trace log source.
+		/// </summary>
+		/// <returns>New log source (or already existing one).</returns>
 		public static ILogSource CreateSource()
 		{
 			if (traceListener == null)
@@ -24,16 +31,21 @@ namespace BepInEx.Logging
 			return traceListener.LogSource;
 		}
 
+		/// <summary>
+		/// Internal log source.
+		/// </summary>
 		protected ManualLogSource LogSource { get; }
 
-		/// <param name="logger">The logger instance to write to.</param>
+		/// <summary>
+		/// Creates a new trace log source.
+		/// </summary>
 		protected TraceLogSource()
 		{
 			LogSource = new ManualLogSource("Trace");
 		}
 
 		/// <summary>
-		/// Writes a message to the underlying <see cref="BaseLogger"/> instance.
+		/// Writes a message to the underlying <see cref="ManualLogSource"/> instance.
 		/// </summary>
 		/// <param name="message">The message to write.</param>
 		public override void Write(string message)
@@ -42,7 +54,7 @@ namespace BepInEx.Logging
 		}
 
 		/// <summary>
-		/// Writes a message and a newline to the underlying <see cref="BaseLogger"/> instance.
+		/// Writes a message and a newline to the underlying <see cref="ManualLogSource"/> instance.
 		/// </summary>
 		/// <param name="message">The message to write.</param>
 		public override void WriteLine(string message)
@@ -50,9 +62,11 @@ namespace BepInEx.Logging
 			LogSource.LogInfo(message);
 		}
 
+		/// <inheritdoc />
 		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));
 
+		/// <inheritdoc />
 		public override void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id, string message)
 		{
 			LogLevel level;

+ 7 - 1
BepInEx.Core/Paths.cs

@@ -1,5 +1,6 @@
 using System.IO;
 using System.Reflection;
+using MonoMod.Utils;
 
 namespace BepInEx
 {
@@ -12,7 +13,11 @@ namespace BepInEx
 		{
 			ExecutablePath = executablePath;
 			ProcessName = Path.GetFileNameWithoutExtension(executablePath);
-			GameRootPath = Path.GetDirectoryName(executablePath);
+
+			GameRootPath = Utility.CurrentPlatform == Platform.MacOS
+				? Utility.ParentDirectory(executablePath, 4)
+				: Path.GetDirectoryName(executablePath);
+
 			BepInExRootPath = bepinRootPath ?? Path.Combine(GameRootPath, "BepInEx");
 			ConfigPath = Path.Combine(BepInExRootPath, "config");
 			BepInExConfigPath = Path.Combine(ConfigPath, "BepInEx.cfg");
@@ -50,6 +55,7 @@ namespace BepInEx
 
 		/// <summary>
 		///     The directory that the currently executing process resides in.
+		///		<para>On OSX however, this is the parent directory of the game.app folder.</para>
 		/// </summary>
 		public static string GameRootPath { get; private set; }
 

+ 1 - 0
BepInEx.Core/Properties/AssemblyInfo.cs

@@ -26,6 +26,7 @@ using BepInEx;
 
 [assembly: InternalsVisibleTo("BepInEx.Preloader.Core")]
 [assembly: InternalsVisibleTo("BepInEx.Unity")]
+[assembly: InternalsVisibleTo("BepInEx.NetLauncher")]
 [assembly: InternalsVisibleTo("BepInExTests")]
 
 // Version information for an assembly consists of the following four values:

+ 91 - 5
BepInEx.Core/Utility.cs

@@ -6,6 +6,7 @@ using System.Reflection;
 using System.Reflection.Emit;
 using System.Text;
 using Mono.Cecil;
+using MonoMod.Utils;
 
 namespace BepInEx
 {
@@ -14,27 +15,35 @@ namespace BepInEx
 	/// </summary>
 	public static class Utility
 	{
+		private static bool? sreEnabled;
+
 		/// <summary>
 		/// Whether current Common Language Runtime supports dynamic method generation using <see cref="System.Reflection.Emit"/> namespace.
 		/// </summary>
-		public static bool CLRSupportsDynamicAssemblies { get; }
+		public static bool CLRSupportsDynamicAssemblies => CheckSRE();
 
-		static Utility()
+		private static bool CheckSRE()
 		{
 			try
 			{
-				CLRSupportsDynamicAssemblies = true;
+				if (sreEnabled.HasValue)
+					return sreEnabled.Value;
+
 				// ReSharper disable once AssignNullToNotNullAttribute
-				var m = new CustomAttributeBuilder(null, new object[0]);
+				_ = new CustomAttributeBuilder(null, new object[0]);
 			}
 			catch (PlatformNotSupportedException)
 			{
-				CLRSupportsDynamicAssemblies = false;
+				sreEnabled = false;
+				return sreEnabled.Value;
 			}
 			catch (ArgumentNullException)
 			{
 				// Suppress ArgumentNullException
 			}
+
+			sreEnabled = true;
+			return sreEnabled.Value;
 		}
 
 		/// <summary>
@@ -66,6 +75,20 @@ namespace BepInEx
         public static string CombinePaths(params string[] parts) => parts.Aggregate(Path.Combine);
 
 		/// <summary>
+		/// Returns the parent directory of a path, optionally specifying the amount of levels.
+		/// </summary>
+		/// <param name="path">The path to get the parent directory of.</param>
+		/// <param name="levels">The amount of levels to traverse. Defaults to 1</param>
+		/// <returns>The parent directory.</returns>
+		public static string ParentDirectory(string path, int levels = 1)
+		{
+			for (int i = 0; i < levels; i++)
+				path = Path.GetDirectoryName(path);
+
+			return path;
+		}
+
+		/// <summary>
 		/// Tries to parse a bool, with a default value if unable to parse.
 		/// </summary>
 		/// <param name="input">The string to parse</param>
@@ -96,6 +119,14 @@ namespace BepInEx
 			return self == null || self.All(Char.IsWhiteSpace);
 		}
 
+		/// <summary>
+		/// Sorts a given dependency graph using a direct toposort, reporting possible cyclic dependencies.
+		/// </summary>
+		/// <param name="nodes">Nodes to sort</param>
+		/// <param name="dependencySelector">Function that maps a node to a collection of its dependencies.</param>
+		/// <typeparam name="TNode">Type of the node in a dependency graph.</typeparam>
+		/// <returns>Collection of nodes sorted in the order of least dependencies to the most.</returns>
+		/// <exception cref="Exception">Thrown when a cyclic dependency occurs.</exception>
 		public static IEnumerable<TNode> TopologicalSort<TNode>(IEnumerable<TNode> nodes, Func<TNode, IEnumerable<TNode>> dependencySelector)
 		{
 			List<TNode> sorted_list = new List<TNode>();
@@ -192,6 +223,12 @@ namespace BepInEx
 			return false;
 		}
 
+		/// <summary>
+		/// Checks whether a given cecil type definition is a subtype of a provided type.
+		/// </summary>
+		/// <param name="self">Cecil type definition</param>
+		/// <param name="td">Type to check against</param>
+		/// <returns>Whether the given cecil type is a subtype of the type.</returns>
 		public static bool IsSubtypeOf(this TypeDefinition self, Type td)
 		{
 			if (self.FullName == td.FullName)
@@ -216,6 +253,7 @@ namespace BepInEx
 		/// </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="readerParameters">Reader parameters that contain possible custom assembly resolver.</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)
@@ -270,5 +308,53 @@ namespace BepInEx
 
 			return builder.ToString();
 		}
+
+		/// <summary>
+		/// Try to parse given string as an assembly name
+		/// </summary>
+		/// <param name="fullName">Fully qualified assembly name</param>
+		/// <param name="assemblyName">Resulting <see cref="AssemblyName"/> instance</param>
+		/// <returns><c>true</c>, if parsing was successful, otherwise <c>false</c></returns>
+		/// <remarks>
+		/// On some versions of mono, using <see cref="Assembly.GetName()"/> fails because it runs on unmanaged side
+		/// which has problems with encoding.
+		/// Using <see cref="AssemblyName"/> solves this by doing parsing on managed side instead.
+		/// </remarks>
+		public static bool TryParseAssemblyName(string fullName, out AssemblyName assemblyName)
+		{
+			try
+			{
+				assemblyName = new AssemblyName(fullName);
+				return true;
+			}
+			catch (Exception)
+			{
+				assemblyName = null;
+				return false;
+			}
+		}
+
+		public static Platform CurrentPlatform { get; private set; } = CheckPlatform();
+
+		// Adapted from https://github.com/MonoMod/MonoMod.Common/blob/master/Utils/PlatformHelper.cs#L13
+		private static Platform CheckPlatform()
+		{
+			var pPlatform = typeof(Environment).GetProperty("Platform", BindingFlags.NonPublic | BindingFlags.Static);
+			string platId = pPlatform != null ? pPlatform.GetValue(null, new object[0]).ToString() : Environment.OSVersion.Platform.ToString();
+			platId = platId.ToLowerInvariant();
+
+			var cur = Platform.Unknown;
+			if (platId.Contains("win"))
+				cur = Platform.Windows;
+			else if (platId.Contains("mac") || platId.Contains("osx"))
+				cur = Platform.MacOS;
+			else if (platId.Contains("lin") || platId.Contains("unix"))
+				cur = Platform.Linux;
+
+			if (IntPtr.Size == 8)
+				cur |= Platform.Bits64;
+
+			return cur;
+		}
 	}
 }

+ 2 - 2
BepInEx.IL2CPP/BepInEx.IL2CPP.csproj

@@ -93,13 +93,13 @@
   </ItemGroup>
   <ItemGroup>
     <PackageReference Include="HarmonyX">
-      <Version>2.1.0-beta.3</Version>
+      <Version>2.1.0-beta.8</Version>
     </PackageReference>
     <PackageReference Include="Iced">
       <Version>1.6.0</Version>
     </PackageReference>
     <PackageReference Include="MonoMod.RuntimeDetour">
-      <Version>20.5.21.5</Version>
+      <Version>20.11.5.1</Version>
     </PackageReference>
   </ItemGroup>
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />

+ 1 - 16
BepInEx.IL2CPP/IL2CPPChainloader.cs

@@ -140,22 +140,7 @@ namespace BepInEx.IL2CPP
 			//}
 
 
-
-
-			// Temporarily disable the console log listener as we replay the preloader logs
-
-			var logListener = Logger.Listeners.FirstOrDefault(logger => logger is ConsoleLogListener);
-
-			if (logListener != null)
-				Logger.Listeners.Remove(logListener);
-
-			foreach (var preloaderLogEvent in PreloaderConsoleListener.LogEvents)
-			{
-				PreloaderLogger.Log.Log(preloaderLogEvent.Level, preloaderLogEvent.Data);
-			}
-
-			if (logListener != null)
-				Logger.Listeners.Add(logListener);
+			ChainloaderLogHelper.RewritePreloaderLogs();
 
 
 			//UnityEngine.Application.s_LogCallbackHandler = DelegateSupport.ConvertDelegate<Application.LogCallback>(new Action<string>(UnityLogCallback));

+ 5 - 3
BepInEx.IL2CPP/Preloader.cs

@@ -21,18 +21,20 @@ namespace BepInEx.IL2CPP
 		{
 			try
 			{
-				PreloaderLog = new PreloaderConsoleListener(true);
+				ConsoleManager.Initialize(false);
+
+				PreloaderLog = new PreloaderConsoleListener();
 				Logger.Listeners.Add(PreloaderLog);
 
 
 
-				if (ConsoleManager.ConfigConsoleEnabled.Value && !ConsoleManager.ConsoleActive)
+				if (ConsoleManager.ConfigConsoleEnabled.Value)
 				{
 					ConsoleManager.CreateConsole();
 					Logger.Listeners.Add(new ConsoleLogListener());
 				}
 
-				BasicLogInfo.PrintLogInfo(Log);
+				ChainloaderLogHelper.PrintLogInfo(Log);
 
 				Log.LogInfo($"Running under Unity v{FileVersionInfo.GetVersionInfo(Paths.ExecutablePath).FileVersion}");
 

+ 1 - 1
BepInEx.NetLauncher/BepInEx.NetLauncher.csproj

@@ -64,7 +64,7 @@
   </ItemGroup>
   <ItemGroup>
     <PackageReference Include="HarmonyX">
-      <Version>2.1.0-beta.3</Version>
+      <Version>2.1.0-beta.8</Version>
     </PackageReference>
   </ItemGroup>
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />

+ 0 - 3
BepInEx.NetLauncher/NetPreloader.cs

@@ -8,7 +8,6 @@ using BepInEx.Configuration;
 using BepInEx.Logging;
 using BepInEx.NetLauncher.RuntimeFixes;
 using BepInEx.Preloader.Core;
-using BepInEx.Preloader.Core.RuntimeFixes;
 using MonoMod.RuntimeDetour;
 
 namespace BepInEx.NetLauncher
@@ -48,8 +47,6 @@ namespace BepInEx.NetLauncher
 
 			Logger.Sources.Add(TraceLogSource.CreateSource());
 
-			HarmonyFixes.Apply();
-
 			string consoleTile = $"BepInEx {typeof(Paths).Assembly.GetName().Version} - {Process.GetCurrentProcess().ProcessName}";
 			Log.LogMessage(consoleTile);
 

+ 1 - 1
BepInEx.NetLauncher/Program.cs

@@ -15,7 +15,7 @@ namespace BepInEx.NetLauncher
 		{
 			Logger.Listeners.Add(new ConsoleLogListener());
 
-			ConsoleManager.ForceSetActive(true);
+			ConsoleManager.Initialize(true);
 
 			NetPreloader.Start(args);
 		}

+ 7 - 6
BepInEx.Preloader.Core/BepInEx.Preloader.Core.csproj

@@ -36,13 +36,13 @@
     <Reference Include="System.Core" />
   </ItemGroup>
   <ItemGroup>
-    <Compile Include="Logging\BasicLogInfo.cs" />
-    <Compile Include="Logging\PreloaderLogWriter.cs" />
+    <Compile Include="Logging\ChainloaderLogHelper.cs" />
+    <Compile Include="Logging\PreloaderConsoleListener.cs" />
     <Compile Include="Patching\AssemblyPatcher.cs" />
     <Compile Include="Patching\PatcherPlugin.cs" />
     <Compile Include="InternalPreloaderLogger.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
-    <Compile Include="RuntimeFixes\HarmonyFixes.cs" />
+    <Compile Include="RuntimeFixes\ConsoleSetOutFix.cs" />
   </ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\BepInEx.Core\BepInEx.Core.csproj">
@@ -52,17 +52,18 @@
   </ItemGroup>
   <ItemGroup>
     <PackageReference Include="HarmonyX">
-      <Version>2.1.0-beta.3</Version>
+      <Version>2.1.0-beta.8</Version>
     </PackageReference>
     <PackageReference Include="Mono.Cecil">
       <Version>0.10.4</Version>
     </PackageReference>
     <PackageReference Include="MonoMod.RuntimeDetour">
-      <Version>20.5.21.5</Version>
+      <Version>20.11.5.1</Version>
     </PackageReference>
     <PackageReference Include="MonoMod.Utils">
-      <Version>20.5.21.5</Version>
+      <Version>20.11.5.1</Version>
     </PackageReference>
   </ItemGroup>
+  <ItemGroup />
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
 </Project>

+ 0 - 26
BepInEx.Preloader.Core/Logging/BasicLogInfo.cs

@@ -1,26 +0,0 @@
-using System.Diagnostics;
-using BepInEx.Logging;
-
-namespace BepInEx.Preloader.Core.Logging
-{
-	public static class BasicLogInfo
-	{
-		public static void PrintLogInfo(ManualLogSource log)
-		{
-			string consoleTile = $"BepInEx {typeof(Paths).Assembly.GetName().Version} - {Process.GetCurrentProcess().ProcessName}";
-			log.LogMessage(consoleTile);
-
-			if (ConsoleManager.ConsoleActive)
-				ConsoleManager.SetConsoleTitle(consoleTile);
-
-			//See BuildInfoAttribute for more information about this section.
-			object[] attributes = typeof(BuildInfoAttribute).Assembly.GetCustomAttributes(typeof(BuildInfoAttribute), false);
-
-			if (attributes.Length > 0)
-			{
-				var attribute = (BuildInfoAttribute)attributes[0];
-				log.LogMessage(attribute.Info);
-			}
-		}
-	}
-}

+ 48 - 0
BepInEx.Preloader.Core/Logging/ChainloaderLogHelper.cs

@@ -0,0 +1,48 @@
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using BepInEx.Logging;
+
+namespace BepInEx.Preloader.Core.Logging
+{
+	public static class ChainloaderLogHelper
+	{
+		public static void PrintLogInfo(ManualLogSource log)
+		{
+			string consoleTitle = $"BepInEx {typeof(Paths).Assembly.GetName().Version} - {Paths.ProcessName}";
+			log.LogMessage(consoleTitle);
+
+			if (ConsoleManager.ConsoleActive)
+				ConsoleManager.SetConsoleTitle(consoleTitle);
+
+			//See BuildInfoAttribute for more information about this section.
+			object[] attributes = typeof(BuildInfoAttribute).Assembly.GetCustomAttributes(typeof(BuildInfoAttribute), false);
+
+			if (attributes.Length > 0)
+			{
+				var attribute = (BuildInfoAttribute)attributes[0];
+				log.LogMessage(attribute.Info);
+			}
+		}
+
+		public static void RewritePreloaderLogs()
+		{
+			if (PreloaderConsoleListener.LogEvents == null || PreloaderConsoleListener.LogEvents.Count == 0)
+				return;
+
+			// Temporarily disable the console log listener as we replay the preloader logs
+			var logListener = Logger.Listeners.FirstOrDefault(logger => logger is ConsoleLogListener);
+
+			if (logListener != null)
+				Logger.Listeners.Remove(logListener);
+
+			foreach (var preloaderLogEvent in PreloaderConsoleListener.LogEvents)
+			{
+				Logger.InternalLogEvent(PreloaderLogger.Log, preloaderLogEvent);
+			}
+
+			if (logListener != null)
+				Logger.Listeners.Add(logListener);
+		}
+	}
+}

+ 34 - 0
BepInEx.Preloader.Core/Logging/PreloaderConsoleListener.cs

@@ -0,0 +1,34 @@
+using System.Collections.Generic;
+using BepInEx.Configuration;
+using BepInEx.Logging;
+
+namespace BepInEx.Preloader.Core.Logging
+{
+	/// <summary>
+	/// Log listener that listens to logs during preloading time and buffers messages for output in Unity logs later.
+	/// </summary>
+	public class PreloaderConsoleListener : ILogListener
+	{
+		/// <summary>
+		/// A list of all <see cref="LogEventArgs"/> objects that this listener has received.
+		/// </summary>
+		public static List<LogEventArgs> LogEvents { get; } = new List<LogEventArgs>();
+
+		/// <inheritdoc />
+		public void LogEvent(object sender, LogEventArgs eventArgs)
+		{
+			if ((eventArgs.Level & ConfigConsoleDisplayedLevel.Value) == 0)
+				return;
+
+			LogEvents.Add(eventArgs);
+		}
+
+		private static readonly ConfigEntry<LogLevel> ConfigConsoleDisplayedLevel = ConfigFile.CoreConfig.Bind(
+			"Logging.Console", "LogLevels",
+			LogLevel.Fatal | LogLevel.Error | LogLevel.Warning | LogLevel.Message | LogLevel.Info,
+			"Which log levels to show in the console output.");
+
+		/// <inheritdoc />
+		public void Dispose() { }
+	}
+}

+ 0 - 90
BepInEx.Preloader.Core/Logging/PreloaderLogWriter.cs

@@ -1,90 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Text;
-using BepInEx.Logging;
-
-namespace BepInEx.Preloader.Core.Logging
-{
-	public class PreloaderConsoleListener : ILogListener
-	{
-		public static List<LogEventArgs> LogEvents { get; } = new List<LogEventArgs>();
-		protected StringBuilder LogBuilder = new StringBuilder();
-
-		public static TextWriter StandardOut { get; set; }
-		protected PreloaderConsoleSource LoggerSource { get; set; }
-
-
-		public PreloaderConsoleListener(bool redirectConsole)
-		{
-			StandardOut = Console.Out;
-
-			if (redirectConsole)
-			{
-				LoggerSource = new PreloaderConsoleSource();
-
-				Logger.Sources.Add(LoggerSource);
-				Console.SetOut(LoggerSource);
-			}
-		}
-
-		public void LogEvent(object sender, LogEventArgs eventArgs)
-		{
-			LogEvents.Add(eventArgs);
-
-			string log = $"[{eventArgs.Level,-7}:{((ILogSource)sender).SourceName,10}] {eventArgs.Data}\r\n";
-
-			LogBuilder.Append(log);
-
-			ConsoleManager.SetConsoleColor(eventArgs.Level.GetConsoleColor());
-			ConsoleDirectWrite(log);
-			ConsoleManager.SetConsoleColor(ConsoleColor.Gray);
-		}
-
-		public void ConsoleDirectWrite(string value)
-		{
-			StandardOut.Write(value);
-		}
-
-		public void ConsoleDirectWriteLine(string value)
-		{
-			StandardOut.WriteLine(value);
-		}
-
-		public override string ToString() => LogBuilder.ToString();
-
-		public void Dispose()
-		{
-			if (LoggerSource != null)
-			{
-				Console.SetOut(StandardOut);
-				Logger.Sources.Remove(LoggerSource);
-				LoggerSource.Dispose();
-				LoggerSource = null;
-			}
-		}
-	}
-
-	public class PreloaderConsoleSource : TextWriter, ILogSource
-	{
-		public override Encoding Encoding { get; } = Console.OutputEncoding;
-
-		public string SourceName { get; } = "BepInEx Preloader";
-
-		public event EventHandler<LogEventArgs> LogEvent;
-
-		public override void Write(object value)
-			=> LogEvent?.Invoke(this, new LogEventArgs(value, LogLevel.Info, this));
-
-		public override void Write(string value)
-			=> Write((object)value);
-
-		public override void WriteLine() { }
-
-		public override void WriteLine(object value)
-			=> Write(value);
-
-		public override void WriteLine(string value)
-			=> Write(value);
-	}
-}

+ 54 - 26
BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs

@@ -31,6 +31,12 @@ namespace BepInEx.Preloader.Core
 		/// </summary>
 		public List<PatcherPlugin> PatcherPlugins { get; } = new List<PatcherPlugin>();
 
+
+		/// <summary>
+		/// A cloned version of <see cref="PatcherPlugins"/> to ensure that any foreach loops do not break when the collection gets modified.
+		/// </summary>
+		private IEnumerable<PatcherPlugin> PatcherPluginsSafe => PatcherPlugins.ToList();
+
 		/// <summary>
 		/// <para>Contains a list of assemblies that will be patched and loaded into the runtime.</para>
 		/// <para>The dictionary has the name of the file, without any directories. These are used by the dumping functionality, and as such, these are also required to be unique. They do not have to be exactly the same as the real filename, however they have to be mapped deterministically.</para>
@@ -87,7 +93,6 @@ namespace BepInEx.Preloader.Core
 		///     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 void AddPatchersFromDirectory(string directory)
 		{
 			if (!Directory.Exists(directory))
@@ -153,8 +158,9 @@ namespace BepInEx.Preloader.Core
 					}
 				}
 
+				var assName = ass.GetName();
 				Logger.Log(patcherCollection.Any() ? LogLevel.Info : LogLevel.Debug,
-					$"Loaded {patcherCollection.Count} patcher methods from {ass.GetName().FullName}");
+					$"Loaded {patcherCollection.Count} patcher method{(patcherCollection.Count == 1 ? "" : "s")} from [{assName.Name} {assName.Version}]");
 			}
 
 			foreach (KeyValuePair<string, PatcherPlugin> patcher in sortedPatchers)
@@ -245,19 +251,6 @@ namespace BepInEx.Preloader.Core
 			PatcherPlugins.Clear();
 		}
 
-		private static string GetAssemblyName(string fullName)
-		{
-			// We need to manually parse full name to avoid issues with encoding on mono
-			try
-			{
-				return new AssemblyName(fullName).Name;
-			}
-			catch (Exception e)
-			{
-				return fullName;
-			}
-		}
-
 		/// <summary>
 		///     Applies patchers to all assemblies in the given directory and loads patched assemblies into memory.
 		/// </summary>
@@ -265,28 +258,54 @@ namespace BepInEx.Preloader.Core
 		public void PatchAndLoad()
 		{
 			// First, create a copy of the assembly dictionary as the initializer can change them
-			var assemblies = new Dictionary<string, AssemblyDefinition>(AssembliesToPatch);
+			var assemblies = new Dictionary<string, AssemblyDefinition>(AssembliesToPatch, StringComparer.InvariantCultureIgnoreCase);
 
 			// Next, initialize all the patchers
-			foreach (var assemblyPatcher1 in PatcherPlugins)
-				assemblyPatcher1.Initializer?.Invoke();
+			foreach (var assemblyPatcher in PatcherPluginsSafe)
+			{
+				try
+				{
+					assemblyPatcher.Initializer?.Invoke();
+				}
+				catch (Exception ex)
+				{
+					Logger.LogError($"Failed to run initializer of {assemblyPatcher.TypeName}: {ex}");
+				}
+			}
 
 			// Then, perform the actual patching
-			var patchedAssemblies = new HashSet<string>();
+
+			var patchedAssemblies = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
 			var resolvedAssemblies = new Dictionary<string, string>();
-			foreach (var assemblyPatcher in PatcherPlugins)
+
+			// TODO: Maybe instead reload the assembly and repatch with other valid patchers?
+			var invalidAssemblies = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
+
+			foreach (var assemblyPatcher in PatcherPluginsSafe)
 				foreach (string targetDll in assemblyPatcher.TargetDLLs())
-					if (AssembliesToPatch.TryGetValue(targetDll, out var assembly))
+					if (AssembliesToPatch.TryGetValue(targetDll, out var assembly) && !invalidAssemblies.Contains(targetDll))
 					{
 						Logger.LogInfo($"Patching [{assembly.Name.Name}] with [{assemblyPatcher.TypeName}]");
 
-						assemblyPatcher.Patcher?.Invoke(ref assembly);
+						try
+						{
+							assemblyPatcher.Patcher?.Invoke(ref assembly);
+						}
+						catch (Exception e)
+						{
+							Logger.LogError($"Failed to run [{assemblyPatcher.TypeName}] when patching [{assembly.Name.Name}]. This assembly will not be patched. Error: {e}");
+							patchedAssemblies.Remove(targetDll);
+							invalidAssemblies.Add(targetDll);
+							continue;
+						}
+
 						AssembliesToPatch[targetDll] = assembly;
 						patchedAssemblies.Add(targetDll);
 
 						foreach (var resolvedAss in AppDomain.CurrentDomain.GetAssemblies())
 						{
-							var name = GetAssemblyName(resolvedAss.FullName);
+							var name = Utility.TryParseAssemblyName(resolvedAss.FullName, out var assName) ? assName.Name : resolvedAss.FullName;
+
 							// Report only the first type that caused the assembly to load, because any subsequent ones can be false positives
 							if (!resolvedAssemblies.ContainsKey(name))
 								resolvedAssemblies[name] = assemblyPatcher.TypeName;
@@ -295,7 +314,7 @@ namespace BepInEx.Preloader.Core
 
 			// Check if any patched assemblies have been already resolved by the CLR
 			// If there are any, they cannot be loaded by the preloader
-			var patchedAssemblyNames = new HashSet<string>(assemblies.Where(kv => patchedAssemblies.Contains(kv.Key)).Select(kv => kv.Value.Name.Name));
+			var patchedAssemblyNames = new HashSet<string>(assemblies.Where(kv => patchedAssemblies.Contains(kv.Key)).Select(kv => kv.Value.Name.Name), StringComparer.InvariantCultureIgnoreCase);
 			var earlyLoadAssemblies = resolvedAssemblies.Where(kv => patchedAssemblyNames.Contains(kv.Key)).ToList();
 
 			if (earlyLoadAssemblies.Count != 0)
@@ -364,8 +383,17 @@ namespace BepInEx.Preloader.Core
 			}
 
 			// Finally, run all finalizers
-			foreach (var assemblyPatcher2 in PatcherPlugins)
-				assemblyPatcher2.Finalizer?.Invoke();
+			foreach (var assemblyPatcher in PatcherPluginsSafe)
+			{
+				try
+				{
+					assemblyPatcher.Finalizer?.Invoke();
+				}
+				catch (Exception ex)
+				{
+					Logger.LogError($"Failed to run finalizer of {assemblyPatcher.TypeName}: {ex}");
+				}
+			}
 		}
 
 		#region Config

+ 2 - 0
BepInEx.Preloader.Core/Patching/PatcherPlugin.cs

@@ -35,11 +35,13 @@ namespace BepInEx.Preloader.Core
 		/// </summary>
 		public string TypeName { get; set; } = string.Empty;
 
+		/// <inheritdoc />
 		public void Save(BinaryWriter bw)
 		{
 			bw.Write(TypeName);
 		}
 
+		/// <inheritdoc />
 		public void Load(BinaryReader br)
 		{
 			TypeName = br.ReadString();

+ 50 - 0
BepInEx.Preloader.Core/RuntimeFixes/ConsoleSetOutFix.cs

@@ -0,0 +1,50 @@
+using System;
+using System.IO;
+using System.Text;
+using BepInEx.Logging;
+using HarmonyLib;
+
+namespace BepInEx.Preloader.RuntimeFixes
+{
+	public static class ConsoleSetOutFix
+	{
+		private static LoggedTextWriter loggedTextWriter;
+		internal static ManualLogSource ConsoleLogSource = Logger.CreateLogSource("Console");
+
+		public static void Apply()
+		{
+			loggedTextWriter = new LoggedTextWriter { Parent = Console.Out };
+			Console.SetOut(loggedTextWriter);
+			HarmonyLib.Harmony.CreateAndPatchAll(typeof(ConsoleSetOutFix));
+		}
+
+		[HarmonyPatch(typeof(Console), nameof(Console.SetOut))]
+		[HarmonyPrefix]
+		private static bool OnSetOut(TextWriter newOut)
+		{
+			loggedTextWriter.Parent = newOut;
+			return false;
+		}
+	}
+
+	internal class LoggedTextWriter : TextWriter
+	{
+		public override Encoding Encoding { get; } = Encoding.UTF8;
+
+		public TextWriter Parent { get; set; }
+
+		public override void Flush() => Parent.Flush();
+
+		public override void Write(string value)
+		{
+			ConsoleSetOutFix.ConsoleLogSource.LogInfo(value);
+			Parent.Write(value);
+		}
+
+		public override void WriteLine(string value)
+		{
+			ConsoleSetOutFix.ConsoleLogSource.LogInfo(value);
+			Parent.WriteLine(value);
+		}
+	}
+}

+ 0 - 36
BepInEx.Preloader.Core/RuntimeFixes/HarmonyFixes.cs

@@ -1,36 +0,0 @@
-using System;
-using System.Diagnostics;
-using HarmonyLib;
-
-namespace BepInEx.Preloader.Core.RuntimeFixes
-{
-	public static class HarmonyFixes
-	{
-		public static void Apply()
-		{
-			try
-			{
-				var harmony = new HarmonyLib.Harmony("BepInEx.Preloader.RuntimeFixes.HarmonyFixes");
-				harmony.Patch(AccessTools.Method(typeof(Traverse), nameof(Traverse.GetValue), new Type[0]), null, new HarmonyMethod(typeof(HarmonyFixes), nameof(GetValue)));
-				harmony.Patch(AccessTools.Method(typeof(Traverse), nameof(Traverse.SetValue), new []{ typeof(object) }), null, new HarmonyMethod(typeof(HarmonyFixes), nameof(SetValue)));
-            }
-			catch (Exception e)
-			{
-				PreloaderLogger.Log.LogError(e);
-			}
-		}
-
-		private static void GetValue(Traverse __instance)
-		{
-			if (!__instance.FieldExists() && !__instance.MethodExists() && !__instance.TypeExists())
-				PreloaderLogger.Log.LogWarning("Traverse.GetValue was called while not pointing at an existing Field, Property, Method or Type. The return value can be unexpected.\n" + new StackTrace());
-		}
-
-		private static void SetValue(Traverse __instance)
-		{
-			// If method exists it will crash inside traverse so only need to mention the field missing
-			if (!__instance.FieldExists() && !__instance.MethodExists())
-				PreloaderLogger.Log.LogWarning("Traverse.SetValue was called while not pointing at an existing Field or Property. The call will have no effect.\n" + new StackTrace());
-		}
-	}
-}

+ 2 - 1
BepInEx.Preloader.Unity/BepInEx.Preloader.Unity.csproj

@@ -38,6 +38,7 @@
   <ItemGroup>
     <Compile Include="DoorstopEntrypoint.cs" />
     <Compile Include="EnvVars.cs" />
+    <Compile Include="RuntimeFixes\XTermFix.cs" />
     <Compile Include="UnityPreloader.cs" />
     <Compile Include="RuntimeFixes\TraceFix.cs" />
     <Compile Include="RuntimeFixes\UnityPatches.cs" />
@@ -55,7 +56,7 @@
   </ItemGroup>
   <ItemGroup>
     <PackageReference Include="HarmonyX">
-      <Version>2.1.0-beta.3</Version>
+      <Version>2.1.0-beta.8</Version>
     </PackageReference>
   </ItemGroup>
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />

+ 75 - 17
BepInEx.Preloader.Unity/DoorstopEntrypoint.cs

@@ -2,27 +2,85 @@
 using System.IO;
 using System.Linq;
 using System.Reflection;
+using BepInEx.Preloader.RuntimeFixes;
 
 namespace BepInEx.Preloader.Unity
 {
 	internal static class UnityPreloaderRunner
 	{
-		public static void PreloaderMain(string[] args)
+		// This is a list of important assemblies in BepInEx core folder that should be force-loaded
+		// Some games can ship these assemblies in Managed folder, in which case assembly resolving bypasses our LocalResolve
+		// On the other hand, renaming these assemblies is not viable because 3rd party assemblies
+		// that we don't build (e.g. MonoMod, Harmony, many plugins) depend on them
+		// As such, we load them early so that the game uses our version instead
+		// These assemblies should be known to be rarely edited and are known to be shipped as-is with Unity assets
+		private static readonly string[] CriticalAssemblies =
 		{
-			string bepinPath = Path.GetDirectoryName(Path.GetDirectoryName(Path.GetFullPath(EnvVars.DOORSTOP_INVOKE_DLL_PATH)));
+			"Mono.Cecil.dll",
+			"Mono.Cecil.Mdb.dll",
+			"Mono.Cecil.Pdb.dll",
+			"Mono.Cecil.Rocks.dll",
+		};
 
-			Paths.SetExecutablePath(args[0], bepinPath);
+		private static void LoadCriticalAssemblies()
+		{
+			foreach (string criticalAssembly in CriticalAssemblies)
+			{
+				try
+				{
+					Assembly.LoadFile(Path.Combine(Paths.BepInExAssemblyDirectory, criticalAssembly));
+				}
+				catch (Exception)
+				{
+					// Suppress error for now
+					// TODO: Should we crash here if load fails? Can't use logging at this point
+				}
+			}
+		}
+
+		public static void PreloaderPreMain()
+		{
+			string bepinPath = Utility.ParentDirectory(Path.GetFullPath(EnvVars.DOORSTOP_INVOKE_DLL_PATH), 2);
+
+			Paths.SetExecutablePath(EnvVars.DOORSTOP_MANAGED_FOLDER_DIR, bepinPath);
 			AppDomain.CurrentDomain.AssemblyResolve += LocalResolve;
+			// Remove temporary resolver early so it won't override local resolver
+			AppDomain.CurrentDomain.AssemblyResolve -= DoorstopEntrypoint.ResolveCurrentDirectory;
+
+			LoadCriticalAssemblies();
+			PreloaderMain();
+		}
+
+		private static void PreloaderMain()
+		{
+			if (UnityPreloader.ConfigApplyRuntimePatches.Value)
+			{
+				XTermFix.Apply();
+				ConsoleSetOutFix.Apply();
+			}
 
 			UnityPreloader.Run(EnvVars.DOORSTOP_MANAGED_FOLDER_DIR);
 		}
 
 		private static Assembly LocalResolve(object sender, ResolveEventArgs args)
 		{
-			var assemblyName = new AssemblyName(args.Name);
+			if (!Utility.TryParseAssemblyName(args.Name, out var assemblyName))
+				return null;
+
+			// Use parse assembly name on managed side because native GetName() can fail on some locales
+			// if the game path has "exotic" characters
+
+			var validAssemblies = AppDomain.CurrentDomain
+										   .GetAssemblies()
+										   .Select(a => new { assembly = a, name = Utility.TryParseAssemblyName(a.FullName, out var name) ? name : null })
+										   .Where(a => a.name != null && a.name.Name == assemblyName.Name)
+										   .OrderByDescending(a => a.name.Version)
+										   .ToList();
 
-			var foundAssembly = AppDomain.CurrentDomain.GetAssemblies()
-										 .FirstOrDefault(x => x.GetName().Name == assemblyName.Name);
+			// First try to match by version, then just pick the best match (generally highest)
+			// This should mainly affect cases where the game itself loads some assembly (like Mono.Cecil) 
+			var foundMatch = validAssemblies.FirstOrDefault(a => a.name.Version == assemblyName.Version) ?? validAssemblies.FirstOrDefault();
+			var foundAssembly = foundMatch?.assembly;
 
 			if (foundAssembly != null)
 				return foundAssembly;
@@ -43,11 +101,7 @@ namespace BepInEx.Preloader.Unity
 		/// <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 Main()
 		{
 			// We set it to the current directory first as a fallback, but try to use the same location as the .exe file.
 			string silentExceptionLog = $"preloader_{DateTime.Now:yyyyMMdd_HHmmss_fff}.log";
@@ -56,7 +110,8 @@ namespace BepInEx.Preloader.Unity
 			{
 				EnvVars.LoadVars();
 
-				silentExceptionLog = Path.Combine(Path.GetDirectoryName(args[0]), silentExceptionLog);
+				string gamePath = Path.GetDirectoryName(EnvVars.DOORSTOP_PROCESS_PATH) ?? ".";
+				silentExceptionLog = Path.Combine(gamePath, silentExceptionLog);
 
 				// 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));
@@ -66,19 +121,22 @@ namespace BepInEx.Preloader.Unity
 				// 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(DoorstopEntrypoint).Assembly.GetType($"BepInEx.Preloader.Unity.{nameof(UnityPreloaderRunner)}")
-								  ?.GetMethod(nameof(UnityPreloaderRunner.PreloaderMain))
-								  ?.Invoke(null, new object[] { args });
-
-				AppDomain.CurrentDomain.AssemblyResolve -= ResolveCurrentDirectory;
+								  ?.GetMethod(nameof(UnityPreloaderRunner.PreloaderPreMain))
+								  ?.Invoke(null, null);
 			}
 			catch (Exception ex)
 			{
 				File.WriteAllText(silentExceptionLog, ex.ToString());
 			}
+			finally
+			{
+				AppDomain.CurrentDomain.AssemblyResolve -= ResolveCurrentDirectory;
+			}
 		}
 
-		private static Assembly ResolveCurrentDirectory(object sender, ResolveEventArgs args)
+		internal static Assembly ResolveCurrentDirectory(object sender, ResolveEventArgs args)
 		{
+			// Can't use Utils here because it's not yet resolved
 			var name = new AssemblyName(args.Name);
 
 			try

+ 6 - 0
BepInEx.Preloader.Unity/EnvVars.cs

@@ -18,10 +18,16 @@ namespace BepInEx.Preloader
 		/// </summary>
 		public static string DOORSTOP_MANAGED_FOLDER_DIR { get; private set; }
 
+		/// <summary>
+		/// Full path to the game executable currently running.
+		/// </summary>
+		public static string DOORSTOP_PROCESS_PATH { get; private set; }
+
 		internal static void LoadVars()
 		{
 			DOORSTOP_INVOKE_DLL_PATH = Environment.GetEnvironmentVariable("DOORSTOP_INVOKE_DLL_PATH");
 			DOORSTOP_MANAGED_FOLDER_DIR = Environment.GetEnvironmentVariable("DOORSTOP_MANAGED_FOLDER_DIR");
+			DOORSTOP_PROCESS_PATH = Environment.GetEnvironmentVariable("DOORSTOP_PROCESS_PATH");
 		}
 	}
 }

+ 1 - 1
BepInEx.Preloader.Unity/RuntimeFixes/TraceFix.cs

@@ -7,7 +7,7 @@ using HarmonyLib;
 namespace BepInEx.Preloader.RuntimeFixes
 {
 	/// <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="BepInEx.Harmony"/> hook.
+	/// This exists because the Mono implementation of <see cref="Trace"/> is/was broken, and would call Write directly instead of calling TraceEvent.
 	/// </summary>
 	internal static class TraceFix
 	{

+ 136 - 0
BepInEx.Preloader.Unity/RuntimeFixes/XTermFix.cs

@@ -0,0 +1,136 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection.Emit;
+using HarmonyLib;
+using MonoMod.RuntimeDetour;
+using MonoMod.RuntimeDetour.Platforms;
+using MonoMod.Utils;
+
+namespace BepInEx.Preloader.RuntimeFixes
+{
+	internal static class XTermFix
+	{
+		public static void Apply()
+		{
+			if (Utility.CurrentPlatform == Platform.Windows)
+				return;
+
+			if (typeof(Console).Assembly.GetType("System.ConsoleDriver") == null)
+			{
+				// Mono version is too old, use our own TTY implementation instead
+				return;
+			}
+
+			if (AccessTools.Method("System.TermInfoReader:DetermineVersion") != null)
+			{
+				// Fix has been applied officially
+				return;
+			}
+			
+			// Apparently on older Unity versions (4.x), using Process.Start can run Console..cctor
+			// And since MonoMod's PlatformHelper (used by DetourHelper.Native) runs Process.Start to determine ARM/x86 platform,
+			// this causes a crash owing to TermInfoReader running before it can be patched and fixed
+			// Because Doorstop does not support ARM at the moment, we can get away with just forcing x86 detour platform.
+			// TODO: Figure out a way to detect ARM on Unix without running Process.Start
+			DetourHelper.Native = new DetourNativeX86Platform();
+
+			var harmony = new HarmonyLib.Harmony("com.bepinex.xtermfix");
+
+			harmony.Patch(AccessTools.Method("System.TermInfoReader:ReadHeader"),
+				prefix: new HarmonyMethod(typeof(XTermFix), nameof(ReadHeaderPrefix)));
+
+			harmony.Patch(AccessTools.Method("System.TermInfoReader:Get", new []{ AccessTools.TypeByName("System.TermInfoNumbers") }),
+				transpiler: new HarmonyMethod(typeof(XTermFix), nameof(GetTermInfoNumbersTranspiler)));
+
+			harmony.Patch(AccessTools.Method("System.TermInfoReader:Get", new []{ AccessTools.TypeByName("System.TermInfoStrings") }),
+				transpiler: new HarmonyMethod(typeof(XTermFix), nameof(GetTermInfoStringsTranspiler)));
+
+			harmony.Patch(AccessTools.Method("System.TermInfoReader:GetStringBytes", new []{ AccessTools.TypeByName("System.TermInfoStrings") }),
+				transpiler: new HarmonyMethod(typeof(XTermFix), nameof(GetTermInfoStringsTranspiler)));
+
+			DetourHelper.Native = null;
+		}
+
+		public static int intOffset;
+
+		public static int GetInt32(byte[] buffer, int offset)
+		{
+			int b1 = buffer[offset];
+			int b2 = buffer[offset + 1];
+			int b3 = buffer[offset + 2];
+			int b4 = buffer[offset + 3];
+
+			return b1 | (b2 << 8) | (b3 << 16) | (b4 << 24);
+		}
+
+		public static short GetInt16(byte[] buffer, int offset)
+		{
+			int b1 = buffer[offset];
+			int b2 = buffer[offset + 1];
+
+			return (short)(b1 | (b2 << 8));
+		}
+
+		public static int GetInteger(byte[] buffer, int offset)
+		{
+			return intOffset == 2
+				? GetInt16(buffer, offset)
+				: GetInt32(buffer, offset);
+		}
+
+		public static void DetermineVersion(short magic)
+		{
+			if (magic == 0x11a)
+				intOffset = 2;
+			else if (magic == 0x21e)
+				intOffset = 4;
+			else
+				throw new Exception($"Unknown xterm header format: {magic}");
+		}
+
+		public static bool ReadHeaderPrefix(byte[] buffer, ref int position, ref short ___boolSize, ref short ___numSize, ref short ___strOffsets)
+		{
+			short magic = GetInt16(buffer, position);
+			position += 2;
+			DetermineVersion(magic);
+
+			// nameSize = GetInt16(buffer, position);
+			position += 2;
+			___boolSize = GetInt16(buffer, position);
+			position += 2;
+			___numSize = GetInt16(buffer, position);
+			position += 2;
+			___strOffsets = GetInt16(buffer, position);
+			position += 2;
+			// strSize = GetInt16(buffer, position);
+			position += 2;
+
+			return false;
+		}
+
+		public static IEnumerable<CodeInstruction> GetTermInfoNumbersTranspiler(IEnumerable<CodeInstruction> instructions)
+		{
+			// This implementation does not seem to have changed so I will be using indexes like the lazy fuck I am
+
+			var list = instructions.ToList();
+
+			list[31] = new CodeInstruction(OpCodes.Ldsfld, AccessTools.Field(typeof(XTermFix), nameof(intOffset)));
+			list[36] = new CodeInstruction(OpCodes.Nop);
+			list[39] = new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(XTermFix), nameof(GetInteger)));
+
+			return list;
+		}
+
+		public static IEnumerable<CodeInstruction> GetTermInfoStringsTranspiler(IEnumerable<CodeInstruction> instructions)
+		{
+			// This implementation does not seem to have changed so I will be using indexes like the lazy fuck I am
+
+			var list = instructions.ToList();
+
+			list[32] = new CodeInstruction(OpCodes.Ldsfld, AccessTools.Field(typeof(XTermFix), nameof(intOffset)));
+
+			return list;
+		}
+	}
+}

+ 71 - 52
BepInEx.Preloader.Unity/UnityPreloader.cs

@@ -1,20 +1,19 @@
 using System;
 using System.Collections.Generic;
+using System.ComponentModel;
 using System.Diagnostics;
 using System.IO;
 using System.Linq;
-using System.Text;
 using BepInEx.Bootstrap;
 using BepInEx.Configuration;
 using BepInEx.Core.Logging;
 using BepInEx.Logging;
 using BepInEx.Preloader.Core;
 using BepInEx.Preloader.Core.Logging;
-using BepInEx.Preloader.Core.RuntimeFixes;
 using BepInEx.Preloader.RuntimeFixes;
 using Mono.Cecil;
 using Mono.Cecil.Cil;
-using MonoMod.RuntimeDetour;
+using MonoMod.Utils;
 using MethodAttributes = Mono.Cecil.MethodAttributes;
 
 namespace BepInEx.Preloader.Unity
@@ -39,35 +38,31 @@ namespace BepInEx.Preloader.Unity
 		{
 			try
 			{
+				InitializeHarmony();
+
+				ConsoleManager.Initialize(false);
 				AllocateConsole();
 
 				if (managedDirectory != null)
 					ManagedPath = managedDirectory;
 
-				bool bridgeInitialized = Utility.TryDo(() =>
+				
+				Utility.TryDo(() =>
 				{
-					if (ConfigShimHarmony.Value)
-						HarmonyDetourBridge.Init();
-				}, out var harmonyBridgeException);
-
-				Exception runtimePatchException = null;
-				if(bridgeInitialized)
-					Utility.TryDo(() =>
-					{
-						if (ConfigApplyRuntimePatches.Value)
-							UnityPatches.Apply();
-					}, out runtimePatchException);
+					if (ConfigApplyRuntimePatches.Value)
+						UnityPatches.Apply();
+				}, out var runtimePatchException);
 
 				Logger.Sources.Add(TraceLogSource.CreateSource());
+				Logger.Sources.Add(new HarmonyLogSource());
 
-				HarmonyFixes.Apply();
-
-				PreloaderLog = new PreloaderConsoleListener(ConfigPreloaderCOutLogging.Value);
+				Logger.Listeners.Add(new ConsoleLogListener());
+				PreloaderLog = new PreloaderConsoleListener();
 				Logger.Listeners.Add(PreloaderLog);
 
-				BasicLogInfo.PrintLogInfo(Log);
+				ChainloaderLogHelper.PrintLogInfo(Log);
 
-				Log.LogInfo($"Running under Unity v{FileVersionInfo.GetVersionInfo(Paths.ExecutablePath).FileVersion}");
+				Log.LogInfo($"Running under Unity v{GetUnityVersion()}");
 				Log.LogInfo($"CLR runtime version: {Environment.Version}");
 				Log.LogInfo($"Supports SRE: {Utility.CLRSupportsDynamicAssemblies}");
 
@@ -75,9 +70,6 @@ namespace BepInEx.Preloader.Unity
 				Log.LogDebug($"Unity Managed directory: {ManagedPath}");
 				Log.LogDebug($"BepInEx root path: {Paths.BepInExRootPath}");
 
-				if (harmonyBridgeException != null)
-					Log.LogWarning($"Failed to enable fix for Harmony for .NET Standard API. Error message: {harmonyBridgeException.Message}");
-
 				if (runtimePatchException != null)
 					Log.LogWarning($"Failed to apply runtime patches for Mono. See more info in the output log. Error message: {runtimePatchException.Message}");
 
@@ -96,7 +88,7 @@ namespace BepInEx.Preloader.Unity
 
 					assemblyPatcher.AddPatchersFromDirectory(Paths.PatcherPluginPath);
 
-					Log.LogInfo($"{assemblyPatcher.PatcherPlugins.Count} patcher plugin(s) loaded");
+					Log.LogInfo($"{assemblyPatcher.PatcherPlugins.Count} patcher plugin{(assemblyPatcher.PatcherPlugins.Count == 1 ? "" : "s")} loaded");
 
 					assemblyPatcher.LoadAssemblyDirectory(ManagedPath);
 
@@ -109,7 +101,6 @@ namespace BepInEx.Preloader.Unity
 				Log.LogMessage("Preloader finished");
 
 				Logger.Listeners.Remove(PreloaderLog);
-				Logger.Listeners.Add(new ConsoleLogListener());
 
 				PreloaderLog.Dispose();
 
@@ -122,26 +113,32 @@ namespace BepInEx.Preloader.Unity
 					Log.LogFatal("Could not run preloader!");
 					Log.LogFatal(ex);
 
-					PreloaderLog?.Dispose();
-
 					if (!ConsoleManager.ConsoleActive)
 					{
 						//if we've already attached the console, then the log will already be written to the console
 						AllocateConsole();
 						Console.Write(PreloaderLog);
 					}
-
-					PreloaderLog = null;
 				}
-				finally
+				catch { }
+
+				string log = string.Empty;
+
+				try
 				{
-					File.WriteAllText(
-						Path.Combine(Paths.GameRootPath, $"preloader_{DateTime.Now:yyyyMMdd_HHmmss_fff}.log"),
-						PreloaderLog + "\r\n" + ex);
+					// We could use platform-dependent newlines, however the developers use Windows so this will be easier to read :)
+
+					log = string.Join("\r\n", PreloaderConsoleListener.LogEvents.Select(x => x.ToString()).ToArray());
+					log += "\r\n";
 
 					PreloaderLog?.Dispose();
 					PreloaderLog = null;
 				}
+				catch { }
+
+				File.WriteAllText(
+					Path.Combine(Paths.GameRootPath, $"preloader_{DateTime.Now:yyyyMMdd_HHmmss_fff}.log"),
+					log + ex);
 			}
 		}
 
@@ -163,7 +160,7 @@ namespace BepInEx.Preloader.Unity
 			var entryType = assembly.MainModule.Types.FirstOrDefault(x => x.Name == entrypointType);
 
 			if (entryType == null)
-				throw new Exception("The entrypoint type is invalid! Please check your config.ini");
+				throw new Exception("The entrypoint type is invalid! Please check your config/BepInEx.cfg file");
 
 			string chainloaderAssemblyPath = Path.Combine(Paths.BepInExAssemblyDirectory, "BepInEx.Unity.dll");
 
@@ -217,7 +214,7 @@ namespace BepInEx.Preloader.Unity
 					il.InsertBefore(ins,
 						il.Create(OpCodes.Ldnull)); // gameExePath (always null, we initialize the Paths class in Entrypoint
 
-                    il.InsertBefore(ins,
+					il.InsertBefore(ins,
 						il.Create(OpCodes.Call, startMethod)); // UnityChainloader.StaticStart(string gameExePath)
 				}
 			}
@@ -234,13 +231,6 @@ namespace BepInEx.Preloader.Unity
 			try
 			{
 				ConsoleManager.CreateConsole();
-
-				var encoding = (uint)Encoding.UTF8.CodePage;
-
-				if (ConsoleManager.ConfigConsoleShiftJis.Value)
-					encoding = 932;
-
-				ConsoleManager.SetConsoleEncoding(encoding);
 			}
 			catch (Exception ex)
 			{
@@ -249,6 +239,39 @@ namespace BepInEx.Preloader.Unity
 			}
 		}
 
+		public static string GetUnityVersion()
+		{
+			if (Utility.CurrentPlatform == Platform.Windows)
+				return FileVersionInfo.GetVersionInfo(Paths.ExecutablePath).FileVersion;
+
+			return $"Unknown ({(IsPostUnity2017 ? "post" : "pre")}-2017)";
+		}
+
+		private static void InitializeHarmony()
+		{
+			switch (ConfigHarmonyBackend.Value)
+			{
+				case MonoModBackend.auto:
+					break;
+				case MonoModBackend.dynamicmethod:
+				case MonoModBackend.methodbuilder:
+				case MonoModBackend.cecil:
+					Environment.SetEnvironmentVariable("MONOMOD_DMD_TYPE", ConfigHarmonyBackend.Value.ToString());
+					break;
+				default:
+					throw new ArgumentOutOfRangeException(nameof(ConfigHarmonyBackend), ConfigHarmonyBackend.Value, "Unknown backend");
+			}
+		}
+
+		private enum MonoModBackend
+		{
+			// Enum names are important!
+			[Description("Auto")] auto = 0,
+			[Description("DynamicMethod")] dynamicmethod,
+			[Description("MethodBuilder")] methodbuilder,
+			[Description("Cecil")] cecil
+		}
+
 		#region Config
 
 		private static readonly ConfigEntry<string> ConfigEntrypointAssembly = ConfigFile.CoreConfig.Bind(
@@ -266,20 +289,16 @@ namespace BepInEx.Preloader.Unity
 			".cctor",
 			"The name of the method in the specified entrypoint assembly and type to hook and load Chainloader from.");
 
-		private static readonly ConfigEntry<bool> ConfigApplyRuntimePatches = ConfigFile.CoreConfig.Bind(
+		internal static readonly ConfigEntry<bool> ConfigApplyRuntimePatches = ConfigFile.CoreConfig.Bind(
 			"Preloader", "ApplyRuntimePatches",
 			true,
 			"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.");
 
-		private static readonly ConfigEntry<bool> ConfigShimHarmony = ConfigFile.CoreConfig.Bind(
-			"Preloader", "ShimHarmonySupport",
-			!Utility.CLRSupportsDynamicAssemblies,
-			"If enabled, basic Harmony functionality is patched to use MonoMod's RuntimeDetour instead.\nTry using this if Harmony does not work in a game.");
-
-		private static readonly ConfigEntry<bool> ConfigPreloaderCOutLogging = ConfigFile.CoreConfig.Bind(
-			"Logging", "PreloaderConsoleOutRedirection",
-			true,
-			"Redirects text from Console.Out during preloader patch loading to the BepInEx logging system.");
+		private static readonly ConfigEntry<MonoModBackend> ConfigHarmonyBackend = ConfigFile.CoreConfig.Bind(
+			"Preloader",
+			"HarmonyBackend",
+			MonoModBackend.auto,
+			"Specifies which MonoMod backend to use for Harmony patches. Auto uses the best available backend.\nThis setting should only be used for development purposes (e.g. debugging in dnSpy). Other code might override this setting.");
 
 		#endregion
 	}

+ 1 - 1
BepInEx.Unity/BepInEx.Unity.csproj

@@ -66,7 +66,7 @@
   </ItemGroup>
   <ItemGroup>
     <PackageReference Include="HarmonyX">
-      <Version>2.1.0-beta.3</Version>
+      <Version>2.1.0-beta.8</Version>
     </PackageReference>
   </ItemGroup>
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />

+ 24 - 36
BepInEx.Unity/Bootstrap/UnityChainloader.cs

@@ -1,15 +1,14 @@
 using BepInEx.Configuration;
 using BepInEx.Logging;
-using System;
 using System.Diagnostics;
 using System.IO;
-using System.Linq;
 using System.Reflection;
+using System.Runtime.CompilerServices;
 using System.Text;
 using BepInEx.Bootstrap;
-using BepInEx.Preloader.Core;
 using BepInEx.Preloader.Core.Logging;
 using BepInEx.Unity.Logging;
+using MonoMod.Utils;
 using UnityEngine;
 using Logger = BepInEx.Logging.Logger;
 
@@ -35,6 +34,20 @@ namespace BepInEx.Unity.Bootstrap
 		private string _consoleTitle;
 		protected override string ConsoleTitle => _consoleTitle;
 
+
+		// In some rare cases calling Application.unityVersion seems to cause MissingMethodException
+		// if a preloader patch applies Harmony patch to Chainloader.Initialize.
+		// The issue could be related to BepInEx being compiled against Unity 5.6 version of UnityEngine.dll,
+		// but the issue is apparently present with both official Harmony and HarmonyX
+		// We specifically prevent inlining to prevent early resolving
+		// TODO: Figure out better version obtaining mechanism (e.g. from globalmanagers)
+		private static string UnityVersion
+		{
+			[MethodImpl(MethodImplOptions.NoInlining)]
+			get => Application.unityVersion;
+		}
+
+
 		public override void Initialize(string gameExePath = null)
 		{
 			UnityTomlTypeConverters.AddUnityEngineConverters();
@@ -45,38 +58,22 @@ namespace BepInEx.Unity.Bootstrap
 			UnityEngine.Object.DontDestroyOnLoad(ManagerObject);
 
 			var productNameProp = typeof(Application).GetProperty("productName", BindingFlags.Public | BindingFlags.Static);
-			_consoleTitle = $"{CurrentAssemblyName} {CurrentAssemblyVersion} - {productNameProp?.GetValue(null, null) ?? Process.GetCurrentProcess().ProcessName}";
+			_consoleTitle = $"{CurrentAssemblyName} {CurrentAssemblyVersion} - {productNameProp?.GetValue(null, null) ?? Path.GetFileNameWithoutExtension(Process.GetCurrentProcess().ProcessName)}";
 
 			base.Initialize(gameExePath);
 		}
 
 		protected override void InitializeLoggers()
 		{
-			if (ConsoleManager.ConfigConsoleEnabled.Value)
-			{
-				ConsoleManager.CreateConsole();
+			base.InitializeLoggers();
 
-				if (!Logger.Listeners.Any(x => x is ConsoleLogListener))
-					Logger.Listeners.Add(new ConsoleLogListener());
-			}
+			Logger.Listeners.Add(new UnityLogListener());
 
-			// Fix for standard output getting overwritten by UnityLogger
-			if (ConsoleManager.StandardOut != null)
+			if (Utility.CurrentPlatform != Platform.Windows)
 			{
-				Console.SetOut(ConsoleManager.StandardOut);
-
-				var encoding = ConsoleManager.ConfigConsoleShiftJis.Value ? 932 : (uint)Encoding.UTF8.CodePage;
-				ConsoleManager.SetConsoleEncoding(encoding);
+				Logger.LogInfo($"Detected Unity version: v{UnityVersion}");
 			}
 
-			Logger.Listeners.Add(new UnityLogListener());
-
-			if (ConfigUnityLogging.Value)
-				Logger.Sources.Add(new UnityLogSource());
-
-
-			base.InitializeLoggers();
-
 
 			if (!ConfigDiskWriteUnityLog.Value)
 			{
@@ -84,20 +81,11 @@ namespace BepInEx.Unity.Bootstrap
 			}
 
 
-			// Temporarily disable the console log listener as we replay the preloader logs
+			ChainloaderLogHelper.RewritePreloaderLogs();
 
-			var logListener = Logger.Listeners.FirstOrDefault(logger => logger is ConsoleLogListener);
 
-			if (logListener != null)
-				Logger.Listeners.Remove(logListener);
-
-			foreach (var preloaderLogEvent in PreloaderConsoleListener.LogEvents)
-			{
-				PreloaderLogger.Log.Log(preloaderLogEvent.Level, preloaderLogEvent.Data);
-			}
-
-			if (logListener != null)
-				Logger.Listeners.Add(logListener);
+			if (ConfigUnityLogging.Value)
+				Logger.Sources.Add(new UnityLogSource());
 		}
 
 		public override BaseUnityPlugin LoadPlugin(PluginInfo pluginInfo, Assembly pluginAssembly)

+ 14 - 4
BepInEx.Unity/Logging/UnityLogListener.cs

@@ -2,6 +2,8 @@
 using System;
 using System.Reflection;
 using System.Runtime.CompilerServices;
+using System.Text;
+using BepInEx.Configuration;
 
 namespace BepInEx.Unity.Logging
 {
@@ -33,17 +35,25 @@ namespace BepInEx.Unity.Logging
 				Logger.LogError("Unable to start Unity log writer");
 		}
 
+		/// <inheritdoc />
 		public void LogEvent(object sender, LogEventArgs eventArgs)
 		{
-			if (sender is UnityLogSource)
+			if (eventArgs.Source is UnityLogSource)
 				return;
 
-			string log = $"[{eventArgs.Level}:{((ILogSource)sender).SourceName}] {eventArgs.Data}\r\n";
-
-			WriteStringToUnityLog?.Invoke(log);
+			// Special case: don't write console twice since Unity can already do that
+			if (LogConsoleToUnity.Value || eventArgs.Source.SourceName != "Console")
+				WriteStringToUnityLog?.Invoke(eventArgs.ToStringLine());
 		}
 
+		/// <inheritdoc />
 		public void Dispose() { }
+
+		private readonly ConfigEntry<bool> LogConsoleToUnity = ConfigFile.CoreConfig.Bind("Logging",
+			"LogConsoleToUnityLog", false,
+			new StringBuilder()
+				.AppendLine("If enabled, writes Standard Output messages to Unity log")
+				.AppendLine("NOTE: By default, Unity does so automatically. Only use this option if no console messages are visible in Unity log").ToString());
 	}
 }
 

+ 10 - 3
BepInEx.Unity/Logging/UnityLogSource.cs

@@ -10,15 +10,21 @@ namespace BepInEx.Unity.Logging
 	/// </summary>
 	public class UnityLogSource : ILogSource
 	{
+		/// <inheritdoc />
 		public string SourceName { get; } = "Unity Log";
+
+		/// <inheritdoc />
 		public event EventHandler<LogEventArgs> LogEvent;
 
+		/// <summary>
+		/// Creates a new Unity log source.
+		/// </summary>
 		public UnityLogSource()
 		{
-			InternalUnityLogMessage += unityLogMessageHandler;
+			InternalUnityLogMessage += UnityLogMessageHandler;
 		}
 
-		private void unityLogMessageHandler(object sender, LogEventArgs eventArgs)
+		private void UnityLogMessageHandler(object sender, LogEventArgs eventArgs)
 		{
 			var newEventArgs = new LogEventArgs(eventArgs.Data, eventArgs.Level, this);
 			LogEvent?.Invoke(this, newEventArgs);
@@ -26,11 +32,12 @@ namespace BepInEx.Unity.Logging
 
 		private bool disposed = false;
 
+		/// <inheritdoc />
 		public void Dispose()
 		{
 			if (!disposed)
 			{
-				InternalUnityLogMessage -= unityLogMessageHandler;
+				InternalUnityLogMessage -= UnityLogMessageHandler;
 				disposed = true;
 			}
 		}

+ 1 - 1
BepInEx.Unity/ThreadingHelper.cs

@@ -214,7 +214,7 @@ namespace BepInEx
 		public static IEnumerable<TOut> RunParallel<TIn, TOut>(this IList<TIn> data, Func<TIn, TOut> work, int workerCount = -1)
 		{
 			if (workerCount < 0)
-				workerCount = Mathf.Max(2, SystemInfo.processorCount);
+				workerCount = Mathf.Max(2, Environment.ProcessorCount);
 			else if (workerCount == 0)
 				throw new ArgumentException("Need at least 1 worker", nameof(workerCount));
 

+ 2 - 0
CODE_OF_CONDUCT.md

@@ -0,0 +1,2 @@
+Don't be a jackass.
+Being a horse is fine, though.

+ 4 - 3
README.md

@@ -17,13 +17,14 @@ Unity plugin framework
 
 **[Bleeding Edge builds](https://builds.bepis.io/projects/bepinex_be)**
 
-**[How to install](https://github.com/bbepis/BepInEx/wiki/How-to-install)**
+**[How to install](https://bepinex.github.io/bepinex_docs/master/articles/user_guide/installation/index.html)**
 
-**[User and developer guides](https://github.com/BepInEx/BepInEx/wiki)**
+**[User and developer guides](https://bepinex.github.io/bepinex_docs/master/articles/index.html)**
 
 ## Used libraries
 - [NeighTools/UnityDoorstop](https://github.com/NeighTools/UnityDoorstop) - 4.0 beta ([99879a9](https://github.com/NeighTools/UnityDoorstop/commit/99879a927bcea6abf16e5f4c7cae220519d07a23))
-- [BepInEx/HarmonyX](https://github.com/BepInEx/HarmonyX) - v2.1.0-beta.3 ([04dd77c](https://github.com/BepInEx/HarmonyX/commit/04dd77c17e9e07e10e6a94b47fff2b43eafb97c6))
+- [NeighTools/UnityDoorstop.Unix](https://github.com/NeighTools/UnityDoorstop.Unix) - 1.2.0.0 ([94c882f](https://github.com/NeighTools/UnityDoorstop.Unix/commit/94c882f9c42b53685571b2d160ccf6e2e9492434))
+- [BepInEx/HarmonyX](https://github.com/BepInEx/HarmonyX) - 2.0.2 ([a278d77](https://github.com/BepInEx/HarmonyX/commit/a278d77bc7fa706838facb0d170db6989564becd))
 - [0x0ade/MonoMod](https://github.com/0x0ade/MonoMod) - v20.05.21.05 ([5d8210d](https://github.com/MonoMod/MonoMod/commit/5d8210d35efb6e85b7b40f1ce040257012936a90))
 - [jbevain/cecil](https://github.com/jbevain/cecil) - 0.10.4 ([98ec890](https://github.com/jbevain/cecil/commit/98ec890d44643ad88d573e97be0e120435eda732))