Browse Source

Merge remote-tracking branch 'remotes/origin/feature-unix-support'

# Conflicts:
#	BepInEx.Preloader/Entrypoint.cs
#	BepInEx.Preloader/Logger/PreloaderLogWriter.cs
#	BepInEx/Bootstrap/Chainloader.cs
#	BepInEx/Logging/ConsoleLogListener.cs
#	BepInEx/Logging/LogEventArgs.cs
#	BepInEx/Logging/Logger.cs
#	BepInEx/Logging/UnityLogListener.cs
#	build.cake
ghorsington 4 years ago
parent
commit
ddd4751039
33 changed files with 1433 additions and 397 deletions
  1. 3 3
      BepInEx.Preloader/BepInEx.Preloader.csproj
  2. 14 5
      BepInEx.Preloader/Entrypoint.cs
  3. 82 83
      BepInEx.Preloader/Logger/PreloaderLogWriter.cs
  4. 37 27
      BepInEx.Preloader/Preloader.cs
  5. 2 2
      BepInEx.Preloader/Properties/AssemblyInfo.cs
  6. 125 0
      BepInEx.Preloader/RuntimeFixes/XTermFix.cs
  7. 1 1
      BepInEx.Preloader/packages.config
  8. 20 6
      BepInEx/BepInEx.csproj
  9. 22 15
      BepInEx/Bootstrap/Chainloader.cs
  10. 138 0
      BepInEx/Console/ConsoleManager.cs
  11. 24 0
      BepInEx/Console/IConsoleDriver.cs
  12. 46 9
      BepInEx/ConsoleUtil/SafeConsole.cs
  13. 20 0
      BepInEx/Console/Unix/ConsoleWriter.cs
  14. 107 0
      BepInEx/Console/Unix/LinuxConsoleDriver.cs
  15. 214 0
      BepInEx/Console/Unix/TtyHandler.cs
  16. 85 0
      BepInEx/Console/Unix/UnixStream.cs
  17. 60 0
      BepInEx/Console/Unix/UnixStreamHelper.cs
  18. 0 0
      BepInEx/Console/Windows/ConsoleEncoding/ConsoleEncoding.Buffers.cs
  19. 0 0
      BepInEx/Console/Windows/ConsoleEncoding/ConsoleEncoding.PInvoke.cs
  20. 0 0
      BepInEx/Console/Windows/ConsoleEncoding/ConsoleEncoding.cs
  21. 13 39
      BepInEx/ConsoleUtil/ConsoleWindow.cs
  22. 11 19
      BepInEx/ConsoleUtil/Kon.cs
  23. 73 0
      BepInEx/Console/Windows/WindowsConsoleDriver.cs
  24. 2 3
      BepInEx/Logging/ConsoleLogListener.cs
  25. 29 29
      BepInEx/Logging/LogEventArgs.cs
  26. 87 87
      BepInEx/Logging/Logger.cs
  27. 56 56
      BepInEx/Logging/UnityLogListener.cs
  28. 7 1
      BepInEx/Paths.cs
  29. 2 2
      BepInEx/Properties/AssemblyInfo.cs
  30. 42 0
      BepInEx/Utility.cs
  31. 1 0
      BepInEx/packages.config
  32. 31 10
      build.cake
  33. 79 0
      doorstop/run_bepinex.sh

+ 3 - 3
BepInEx.Preloader/BepInEx.Preloader.csproj

@@ -39,9 +39,8 @@
       <HintPath>$(SolutionDir)\packages\MonoMod.RuntimeDetour.20.4.3.1\lib\net35\MonoMod.RuntimeDetour.dll</HintPath>
       <Private>True</Private>
     </Reference>
-    <Reference Include="MonoMod.Utils, Version=20.4.3.1, Culture=neutral, PublicKeyToken=null">
-      <HintPath>$(SolutionDir)\packages\MonoMod.Utils.20.4.3.1\lib\net35\MonoMod.Utils.dll</HintPath>
-      <Private>True</Private>
+    <Reference Include="MonoMod.Utils, Version=20.5.21.5, Culture=neutral, processorArchitecture=MSIL">
+      <HintPath>..\packages\MonoMod.Utils.20.5.21.5\lib\net35\MonoMod.Utils.dll</HintPath>
     </Reference>
     <Reference Include="System" />
     <Reference Include="System.Core" />
@@ -57,6 +56,7 @@
     <Compile Include="Logger\PreloaderLogWriter.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="RuntimeFixes\UnityPatches.cs" />
+    <Compile Include="RuntimeFixes\XTermFix.cs" />
   </ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\BepInEx\BepInEx.csproj">

+ 14 - 5
BepInEx.Preloader/Entrypoint.cs

@@ -1,19 +1,28 @@
 using System;
-using System.Diagnostics;
 using System.IO;
 using System.Linq;
 using System.Reflection;
+using BepInEx.Preloader.RuntimeFixes;
 
 namespace BepInEx.Preloader
 {
 	internal static class PreloaderRunner
 	{
-		public static void PreloaderMain()
+		public static void PreloaderPreMain()
 		{
-			string bepinPath = Path.GetDirectoryName(Path.GetDirectoryName(Path.GetFullPath(EnvVars.DOORSTOP_INVOKE_DLL_PATH)));
+			string bepinPath = Utility.ParentDirectory(Path.GetFullPath(EnvVars.DOORSTOP_INVOKE_DLL_PATH), 2);
 
 			Paths.SetExecutablePath(EnvVars.DOORSTOP_PROCESS_PATH, bepinPath, EnvVars.DOORSTOP_MANAGED_FOLDER_DIR);
 			AppDomain.CurrentDomain.AssemblyResolve += LocalResolve;
+
+			PreloaderMain();
+		}
+
+		private static void PreloaderMain()
+		{
+			if (Preloader.ConfigApplyRuntimePatches.Value)
+				XTermFix.Apply();
+
 			Preloader.Run();
 		}
 
@@ -49,7 +58,7 @@ namespace BepInEx.Preloader
 		///     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";
@@ -68,7 +77,7 @@ namespace BepInEx.Preloader
 				// In some versions of Unity 4, Mono tries to resolve BepInEx.dll prematurely because of the call to Paths.SetExecutablePath
 				// To prevent that, we have to use reflection and a separate startup class so that we can install required assembly resolvers before the main code
 				typeof(Entrypoint).Assembly.GetType($"BepInEx.Preloader.{nameof(PreloaderRunner)}")
-								  ?.GetMethod(nameof(PreloaderRunner.PreloaderMain))
+								  ?.GetMethod(nameof(PreloaderRunner.PreloaderPreMain))
 								  ?.Invoke(null, null);
 
 				AppDomain.CurrentDomain.AssemblyResolve -= ResolveCurrentDirectory;

+ 82 - 83
BepInEx.Preloader/Logger/PreloaderLogWriter.cs

@@ -1,84 +1,83 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Text;
-using BepInEx.ConsoleUtil;
-using BepInEx.Logging;
-
-namespace BepInEx.Preloader
-{
-	public class PreloaderConsoleListener : ILogListener
-	{
-		public static List<LogEventArgs> LogEvents { get; } = new List<LogEventArgs>();
-
-		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);
-
-			Kon.ForegroundColor = eventArgs.Level.GetConsoleColor();
-			ConsoleDirectWrite(eventArgs.ToStringLine());
-			Kon.ForegroundColor = ConsoleColor.Gray;
-		}
-
-		public void ConsoleDirectWrite(string value)
-		{
-			StandardOut.Write(value);
-		}
-
-		public void ConsoleDirectWriteLine(string value)
-		{
-			StandardOut.WriteLine(value);
-		}
-
-		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);
-	}
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using BepInEx.Logging;
+
+namespace BepInEx.Preloader
+{
+	public class PreloaderConsoleListener : ILogListener
+	{
+		public static List<LogEventArgs> LogEvents { get; } = new List<LogEventArgs>();
+
+		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);
+
+			ConsoleManager.SetConsoleColor(eventArgs.Level.GetConsoleColor());
+			ConsoleDirectWrite(eventArgs.ToStringLine());
+			ConsoleManager.SetConsoleColor(ConsoleColor.Gray);
+		}
+
+		public void ConsoleDirectWrite(string value)
+		{
+			StandardOut.Write(value);
+		}
+
+		public void ConsoleDirectWriteLine(string value)
+		{
+			StandardOut.WriteLine(value);
+		}
+
+		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);
+	}
 }

+ 37 - 27
BepInEx.Preloader/Preloader.cs

@@ -3,7 +3,6 @@ using System.Collections.Generic;
 using System.Diagnostics;
 using System.IO;
 using System.Linq;
-using System.Text;
 using BepInEx.Configuration;
 using BepInEx.Logging;
 using BepInEx.Preloader.Patching;
@@ -12,7 +11,7 @@ using HarmonyLib;
 using Mono.Cecil;
 using Mono.Cecil.Cil;
 using MonoMod.RuntimeDetour;
-using UnityInjector.ConsoleUtil;
+using MonoMod.Utils;
 using MethodAttributes = Mono.Cecil.MethodAttributes;
 
 namespace BepInEx.Preloader
@@ -33,6 +32,7 @@ namespace BepInEx.Preloader
 		{
 			try
 			{
+				ConsoleManager.Initialize(false);
 				AllocateConsole();
 
 				bool bridgeInitialized = Utility.TryDo(() =>
@@ -42,7 +42,7 @@ namespace BepInEx.Preloader
 				}, out var harmonyBridgeException);
 
 				Exception runtimePatchException = null;
-				if(bridgeInitialized)
+				if (bridgeInitialized)
 					Utility.TryDo(() =>
 					{
 						if (ConfigApplyRuntimePatches.Value)
@@ -56,8 +56,11 @@ namespace BepInEx.Preloader
 				PreloaderLog = new PreloaderConsoleListener(ConfigPreloaderCOutLogging.Value);
 				Logger.Listeners.Add(PreloaderLog);
 
-				string consoleTile = $"BepInEx {typeof(Paths).Assembly.GetName().Version} - {Process.GetCurrentProcess().ProcessName}";
-				ConsoleWindow.Title = consoleTile;
+				string consoleTile = $"BepInEx {typeof(Paths).Assembly.GetName().Version} - {Path.GetFileNameWithoutExtension(Process.GetCurrentProcess().ProcessName)}";
+
+				if (ConsoleManager.ConsoleActive)
+					ConsoleManager.SetConsoleTitle(consoleTile);
+
 				Logger.LogMessage(consoleTile);
 
 				//See BuildInfoAttribute for more information about this section.
@@ -69,7 +72,7 @@ namespace BepInEx.Preloader
 					Logger.LogMessage(attribute.Info);
 				}
 
-				Logger.LogInfo($"Running under Unity v{FileVersionInfo.GetVersionInfo(Paths.ExecutablePath).FileVersion}");
+				Logger.LogInfo($"Running under Unity v{GetUnityVersion()}");
 				Logger.LogInfo($"CLR runtime version: {Environment.Version}");
 				Logger.LogInfo($"Supports SRE: {Utility.CLRSupportsDynamicAssemblies}");
 
@@ -110,26 +113,32 @@ namespace BepInEx.Preloader
 					Logger.LogFatal("Could not run preloader!");
 					Logger.LogFatal(ex);
 
-					PreloaderLog?.Dispose();
-
-					if (!ConsoleWindow.IsAttached)
+					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);
 			}
 		}
 
@@ -151,7 +160,7 @@ namespace BepInEx.Preloader
 			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");
 
 			using (var injected = AssemblyDefinition.ReadAssembly(Paths.BepInExAssemblyPath))
 			{
@@ -222,20 +231,13 @@ namespace BepInEx.Preloader
 		/// </summary>
 		public static void AllocateConsole()
 		{
-			if (!ConsoleWindow.ConfigConsoleEnabled.Value)
+			if (!ConsoleManager.ConfigConsoleEnabled.Value)
 				return;
 
 			try
 			{
-				ConsoleWindow.Attach();
-
-				var encoding = (uint)Encoding.UTF8.CodePage;
-
-				if (ConsoleWindow.ConfigConsoleShiftJis.Value)
-					encoding = 932;
-
-				ConsoleEncoding.ConsoleCodePage = encoding;
-				Console.OutputEncoding = ConsoleEncoding.GetEncoding(encoding);
+				ConsoleManager.CreateConsole();
+				ConsoleManager.SetConsoleEncoding();
 			}
 			catch (Exception ex)
 			{
@@ -244,6 +246,14 @@ namespace BepInEx.Preloader
 			}
 		}
 
+		public static string GetUnityVersion()
+		{
+			if (Utility.CurrentOs == Platform.Windows)
+				return FileVersionInfo.GetVersionInfo(Paths.ExecutablePath).FileVersion;
+
+			return $"Unknown ({(IsPostUnity2017 ? "post" : "pre")}-2017)";
+		}
+
 		#region Config
 
 		private static readonly ConfigEntry<string> ConfigEntrypointAssembly = ConfigFile.CoreConfig.Bind(
@@ -261,7 +271,7 @@ namespace BepInEx.Preloader
 			".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.");

+ 2 - 2
BepInEx.Preloader/Properties/AssemblyInfo.cs

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

+ 125 - 0
BepInEx.Preloader/RuntimeFixes/XTermFix.cs

@@ -0,0 +1,125 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection.Emit;
+using HarmonyLib;
+using MonoMod.Utils;
+
+namespace BepInEx.Preloader.RuntimeFixes
+{
+	internal static class XTermFix
+	{
+		public static void Apply()
+		{
+			if (Utility.CurrentOs == 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;
+			}
+
+			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)));
+		}
+
+		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;
+		}
+	}
+}

+ 1 - 1
BepInEx.Preloader/packages.config

@@ -2,5 +2,5 @@
 <packages>
   <package id="Mono.Cecil" version="0.10.4" targetFramework="net35" />
   <package id="MonoMod.RuntimeDetour" version="20.4.3.1" targetFramework="net35" />
-  <package id="MonoMod.Utils" version="20.4.3.1" targetFramework="net35" />
+  <package id="MonoMod.Utils" version="20.5.21.5" targetFramework="net35" />
 </packages>

+ 20 - 6
BepInEx/BepInEx.csproj

@@ -22,6 +22,7 @@
     <ErrorReport>prompt</ErrorReport>
     <WarningLevel>4</WarningLevel>
     <DocumentationFile>..\bin\BepInEx.xml</DocumentationFile>
+    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
   </PropertyGroup>
   <PropertyGroup>
     <StartupObject />
@@ -37,6 +38,7 @@
     <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
     <DebugType>full</DebugType>
     <DebugSymbols>true</DebugSymbols>
+    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
   </PropertyGroup>
   <ItemGroup>
     <Reference Include="Mono.Cecil, Version=0.10.4.0, Culture=neutral, PublicKeyToken=50cebf1cceb9d05e, processorArchitecture=MSIL">
@@ -51,6 +53,9 @@
     <Reference Include="Mono.Cecil.Rocks, Version=0.10.4.0, Culture=neutral, PublicKeyToken=50cebf1cceb9d05e, processorArchitecture=MSIL">
       <HintPath>$(SolutionDir)\packages\Mono.Cecil.0.10.4\lib\net35\Mono.Cecil.Rocks.dll</HintPath>
     </Reference>
+    <Reference Include="MonoMod.Utils, Version=20.5.21.5, Culture=neutral, processorArchitecture=MSIL">
+      <HintPath>..\packages\MonoMod.Utils.20.5.21.5\lib\net35\MonoMod.Utils.dll</HintPath>
+    </Reference>
     <Reference Include="System" />
     <Reference Include="UnityEngine">
       <HintPath>..\lib\UnityEngine.dll</HintPath>
@@ -62,6 +67,14 @@
     <Compile Include="Configuration\AcceptableValueList.cs" />
     <Compile Include="Configuration\AcceptableValueRange.cs" />
     <Compile Include="Configuration\ConfigEntryBase.cs" />
+    <Compile Include="Console\ConsoleManager.cs" />
+    <Compile Include="Console\IConsoleDriver.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\WindowsConsoleDriver.cs" />
     <Compile Include="Contract\PluginInfo.cs" />
     <Compile Include="Configuration\ConfigDefinition.cs" />
     <Compile Include="Configuration\ConfigDescription.cs" />
@@ -72,12 +85,12 @@
     <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="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\SafeConsole.cs" />
     <Compile Include="Bootstrap\Chainloader.cs" />
     <Compile Include="Contract\BaseUnityPlugin.cs" />
     <Compile Include="Logging\DiskLogListener.cs" />
@@ -110,5 +123,6 @@
   <ItemGroup>
     <None Include="packages.config" />
   </ItemGroup>
+  <ItemGroup />
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
 </Project>

+ 22 - 15
BepInEx/Bootstrap/Chainloader.cs

@@ -6,11 +6,10 @@ using System.Diagnostics;
 using System.IO;
 using System.Linq;
 using System.Reflection;
-using System.Text;
 using System.Text.RegularExpressions;
 using Mono.Cecil;
+using MonoMod.Utils;
 using UnityEngine;
-using UnityInjector.ConsoleUtil;
 using Logger = BepInEx.Logging.Logger;
 
 namespace BepInEx.Bootstrap
@@ -69,20 +68,17 @@ namespace BepInEx.Bootstrap
 			}
 
 			// Start logging
-			if (ConsoleWindow.ConfigConsoleEnabled.Value && startConsole)
+			if (ConsoleManager.ConfigConsoleEnabled.Value && startConsole)
 			{
-				ConsoleWindow.Attach();
+				ConsoleManager.CreateConsole();
 				Logger.Listeners.Add(new ConsoleLogListener());
 			}
 
 			// Fix for standard output getting overwritten by UnityLogger
-			if (ConsoleWindow.StandardOut != null)
+			if (ConsoleManager.ConsoleActive)
 			{
-				Console.SetOut(ConsoleWindow.StandardOut);
-
-				var encoding = ConsoleWindow.ConfigConsoleShiftJis.Value ? 932 : (uint)Encoding.UTF8.CodePage;
-				ConsoleEncoding.ConsoleCodePage = encoding;
-				Console.OutputEncoding = ConsoleEncoding.GetEncoding(encoding);
+				ConsoleManager.SetConsoleStreams();
+				ConsoleManager.SetConsoleEncoding();
 			}
 
 			Logger.Listeners.Add(new UnityLogListener());
@@ -109,7 +105,7 @@ namespace BepInEx.Bootstrap
 				var preloaderLogSource = Logger.CreateLogSource("Preloader");
 
 				foreach (var preloaderLogEvent in preloaderLogEvents)
-					Logger.InternalLogEvent(preloaderLogSource, preloaderLogEvent);
+					Logger.InternalLogEvent(preloaderLogSource, preloaderLogEvent);
 
 				Logger.Sources.Remove(preloaderLogSource);	
 			}
@@ -117,6 +113,11 @@ namespace BepInEx.Bootstrap
 			if (logListener != null)
 				Logger.Listeners.Add(logListener);
 
+			if (Utility.CurrentOs == Platform.Linux)
+			{
+				Logger.LogInfo($"Detected Unity version: v{Application.unityVersion}");
+			}
+
 			Logger.LogMessage("Chainloader ready");
 
 			_initialized = true;
@@ -227,7 +228,9 @@ namespace BepInEx.Bootstrap
 			try
 			{
 				var productNameProp = typeof(Application).GetProperty("productName", BindingFlags.Public | BindingFlags.Static);
-				ConsoleWindow.Title = $"{CurrentAssemblyName} {CurrentAssemblyVersion} - {productNameProp?.GetValue(null, null) ?? Process.GetCurrentProcess().ProcessName}";
+
+				if (ConsoleManager.ConsoleActive)
+					ConsoleManager.SetConsoleTitle($"{CurrentAssemblyName} {CurrentAssemblyVersion} - {productNameProp?.GetValue(null, null) ?? Path.GetFileNameWithoutExtension(Process.GetCurrentProcess().ProcessName)}");
 
 				Logger.LogMessage("Chainloader started");
 
@@ -384,10 +387,14 @@ namespace BepInEx.Bootstrap
 			}
 			catch (Exception ex)
 			{
-				ConsoleWindow.Attach();
+				try
+				{
+					ConsoleManager.CreateConsole();
+				}
+				catch { }
 
-				Console.WriteLine("Error occurred starting the game");
-				Console.WriteLine(ex.ToString());
+				Logger.LogFatal("Error occurred starting the game");
+				Logger.LogFatal(ex.ToString());
 			}
 
 			Logger.LogMessage("Chainloader startup complete");

+ 138 - 0
BepInEx/Console/ConsoleManager.cs

@@ -0,0 +1,138 @@
+using System;
+using System.IO;
+using System.Text;
+using BepInEx.Configuration;
+using BepInEx.Unix;
+using MonoMod.Utils;
+
+namespace BepInEx
+{
+	public static class ConsoleManager
+	{
+		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.CurrentOs)
+			{
+				case Platform.MacOS:
+				case Platform.Linux:
+				{
+					Driver = new LinuxConsoleDriver();
+					break;
+				}
+
+				case Platform.Windows:
+				{
+					Driver = new WindowsConsoleDriver();
+					break;
+				}
+			}
+
+			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();
+
+			Driver.CreateConsole();
+			SetConsoleStreams();
+		}
+
+		public static void DetachConsole()
+		{
+			if (!ConsoleActive)
+				return;
+
+			DriverCheck();
+
+			Driver.DetachConsole();
+			SetConsoleStreams();
+		}
+
+		public static void SetConsoleEncoding()
+		{
+			// Apparently Windows code-pages work in Mono.
+			// https://stackoverflow.com/a/33456543
+			// Alternatively we can pass in "shift-jis"
+			var encoding = ConfigConsoleShiftJis.Value ? Encoding.GetEncoding(932): Encoding.UTF8;
+
+			SetConsoleEncoding(encoding);
+		}
+
+		public static void SetConsoleEncoding(Encoding encoding)
+		{
+			if (!ConsoleActive)
+				throw new InvalidOperationException("Console is not currently active");
+
+			DriverCheck();
+
+			Driver.SetConsoleEncoding(encoding);
+		}
+
+		public static void SetConsoleTitle(string title)
+		{
+			DriverCheck();
+
+			Driver.SetConsoleTitle(title);
+		}
+
+		public static void SetConsoleColor(ConsoleColor color)
+		{
+			DriverCheck();
+
+			Driver.SetConsoleColor(color);
+		}
+
+		internal static void SetConsoleStreams()
+		{
+			if (ConsoleActive)
+			{
+				Console.SetOut(ConsoleStream);
+				Console.SetError(ConsoleStream);
+			}
+			else
+			{
+				Console.SetOut(TextWriter.Null);
+				Console.SetError(TextWriter.Null);
+			}
+		}
+
+		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.");
+	}
+}

+ 24 - 0
BepInEx/Console/IConsoleDriver.cs

@@ -0,0 +1,24 @@
+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);
+
+		void CreateConsole();
+		void DetachConsole();
+
+		void SetConsoleColor(ConsoleColor color);
+		void SetConsoleEncoding(Encoding encoding);
+		void SetConsoleTitle(string title);
+	}
+}

+ 46 - 9
BepInEx/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);
 	}
 }

+ 20 - 0
BepInEx/Console/Unix/ConsoleWriter.cs

@@ -0,0 +1,20 @@
+using System.IO;
+using System.Reflection;
+using System.Text;
+using HarmonyLib;
+
+namespace BepInEx.Unix
+{
+	public static class ConsoleWriter
+	{
+		private static ConstructorInfo cStreamWriterConstructor = AccessTools.Constructor(AccessTools.TypeByName("System.IO.CStreamWriter"), new []{ typeof(Stream), typeof(Encoding), typeof(bool) });
+		public static TextWriter CreateConsoleStreamWriter(Stream stream, Encoding encoding, bool leaveOpen)
+		{
+			var writer = (StreamWriter)cStreamWriterConstructor.Invoke(new object[] { stream, encoding, leaveOpen, });
+			
+			writer.AutoFlush = true;
+
+			return writer;
+		}
+	}
+}

+ 107 - 0
BepInEx/Console/Unix/LinuxConsoleDriver.cs

@@ -0,0 +1,107 @@
+using System;
+using System.IO;
+using System.Text;
+using BepInEx.Logging;
+using HarmonyLib;
+using UnityInjector.ConsoleUtil;
+
+namespace BepInEx.Unix
+{
+	internal class LinuxConsoleDriver : IConsoleDriver
+	{
+		public TextWriter StandardOut { get; private set; }
+		public TextWriter ConsoleOut { get; private set;  }
+
+		public bool ConsoleActive { get; private set; }
+		public bool ConsoleIsExternal => false;
+
+		public bool UseMonoTtyDriver { get; private set; }
+		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;
+			
+			UseMonoTtyDriver = typeof(Console).Assembly.GetType("System.ConsoleDriver") != null;
+
+			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()
+		{
+			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 SetConsoleEncoding(Encoding encoding)
+		{
+			// We shouldn't be changing this on Unix
+		}
+
+		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/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/Console/Unix/UnixStream.cs

@@ -0,0 +1,85 @@
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+
+namespace BepInEx.Unix
+{
+	public 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/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", // Linux glibc
+					"libc.so.6", // Ubuntu 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/ConsoleUtil/ConsoleEncoding/ConsoleEncoding.Buffers.cs → BepInEx/Console/Windows/ConsoleEncoding/ConsoleEncoding.Buffers.cs


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


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


+ 13 - 39
BepInEx/ConsoleUtil/ConsoleWindow.cs

@@ -4,38 +4,23 @@
 // --------------------------------------------------
 
 using System;
-using System.IO;
 using System.Runtime.InteropServices;
-using System.Text;
-using BepInEx.Configuration;
 
 namespace UnityInjector.ConsoleUtil
 {
 	internal class ConsoleWindow
 	{
-		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 bool IsAttached { get; private set; }
-		private static IntPtr _cOut;
-		private static IntPtr _oOut;
-
-		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();
@@ -48,12 +33,11 @@ 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");
-			Init();
 
 			IsAttached = true;
 		}
@@ -87,14 +71,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 +117,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/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)

+ 73 - 0
BepInEx/Console/Windows/WindowsConsoleDriver.cs

@@ -0,0 +1,73 @@
+using System;
+using System.IO;
+using System.Text;
+using BepInEx.ConsoleUtil;
+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;
+		}
+
+		public void CreateConsole()
+		{
+			ConsoleWindow.Attach();
+
+			// Not sure if this is needed? Does the original Console.Out still work?
+			var originalOutStream = new FileStream(new SafeFileHandle(ConsoleWindow.OriginalStdoutHandle, false), FileAccess.Write);
+			StandardOut = new StreamWriter(originalOutStream, new UTF8Encoding(false))
+			{
+				AutoFlush = true
+			};
+
+			var consoleOutStream = new FileStream(new SafeFileHandle(ConsoleWindow.ConsoleOutHandle, false), FileAccess.Write);
+			ConsoleOut = new StreamWriter(consoleOutStream, Console.OutputEncoding)
+			{
+				AutoFlush = true
+			};
+
+			ConsoleActive = true;
+		}
+
+		public void DetachConsole()
+		{
+			ConsoleWindow.Detach();
+
+			ConsoleOut.Close();
+			ConsoleOut = null;
+
+			ConsoleActive = false;
+		}
+
+		public void SetConsoleColor(ConsoleColor color)
+		{
+			SafeConsole.ForegroundColor = color;
+			Kon.ForegroundColor = color;
+		}
+
+		public void SetConsoleEncoding(Encoding encoding)
+		{
+			ConsoleEncoding.ConsoleCodePage = (uint)encoding.WindowsCodePage;
+			Console.OutputEncoding = encoding;
+		}
+
+		public void SetConsoleTitle(string title)
+		{
+			ConsoleWindow.Title = title;
+			Console.Title = title;
+		}
+	}
+}

+ 2 - 3
BepInEx/Logging/ConsoleLogListener.cs

@@ -1,6 +1,5 @@
 using System;
 using BepInEx.Configuration;
-using BepInEx.ConsoleUtil;
 
 namespace BepInEx.Logging
 {
@@ -14,9 +13,9 @@ namespace BepInEx.Logging
 			if ((eventArgs.Level & ConfigConsoleDisplayedLevel.Value) == 0)
 				return;
 
-			Kon.ForegroundColor = eventArgs.Level.GetConsoleColor();
+			ConsoleManager.SetConsoleColor(eventArgs.Level.GetConsoleColor());
 			Console.Write(eventArgs.ToStringLine());
-			Kon.ForegroundColor = ConsoleColor.Gray;
+			ConsoleManager.SetConsoleColor(ConsoleColor.Gray);
 		}
 
 		public void Dispose() { }

+ 29 - 29
BepInEx/Logging/LogEventArgs.cs

@@ -1,30 +1,30 @@
-using System;
-
-namespace BepInEx.Logging
-{
-	public class LogEventArgs : EventArgs
-	{
-		public object Data { get; protected set; }
-
-		public LogLevel Level { get; protected set; }
-
-		public ILogSource Source { get; protected set; }
-
-		public LogEventArgs(object data, LogLevel level, ILogSource source)
-		{
-			Data = data;
-			Level = level;
-			Source = source;
-		}
-
-		public override string ToString()
-		{
-			return $"[{Level,-7}:{Source.SourceName,10}] {Data}";
-		}
-
-		public string ToStringLine()
-		{
-			return $"[{Level,-7}:{Source.SourceName,10}] {Data}{Environment.NewLine}";
-		}
-	}
+using System;
+
+namespace BepInEx.Logging
+{
+	public class LogEventArgs : EventArgs
+	{
+		public object Data { get; protected set; }
+
+		public LogLevel Level { get; protected set; }
+
+		public ILogSource Source { get; protected set; }
+
+		public LogEventArgs(object data, LogLevel level, ILogSource source)
+		{
+			Data = data;
+			Level = level;
+			Source = source;
+		}
+
+		public override string ToString()
+		{
+			return $"[{Level,-7}:{Source.SourceName,10}] {Data}";
+		}
+
+		public string ToStringLine()
+		{
+			return $"[{Level,-7}:{Source.SourceName,10}] {Data}{Environment.NewLine}";
+		}
+	}
 }

+ 87 - 87
BepInEx/Logging/Logger.cs

@@ -1,88 +1,88 @@
-using System;
-using System.Collections.Generic;
-
-namespace BepInEx.Logging
-{
-	/// <summary>
-	/// A static <see cref="BaseLogger"/> instance.
-	/// </summary>
-	public static class Logger
-	{
-		public static ICollection<ILogListener> Listeners { get; } = new List<ILogListener>();
-
-		public static ICollection<ILogSource> Sources { get; } = new LogSourceCollection();
-
-		private static readonly ManualLogSource InternalLogSource = CreateLogSource("BepInEx");
-
-		internal static void InternalLogEvent(object sender, LogEventArgs eventArgs)
-		{
-			foreach (var listener in Listeners)
-			{
-				listener?.LogEvent(sender, eventArgs);
-			}
-		}
-
-		/// <summary>
-		/// Logs an entry to the current logger instance.
-		/// </summary>
-		/// <param name="level">The level of the entry.</param>
-		/// <param name="entry">The textual value of the entry.</param>
-		internal static void Log(LogLevel level, object data)
-		{
-			InternalLogSource.Log(level, data);
-		}
-
-		internal static void LogFatal(object data) => Log(LogLevel.Fatal, data);
-		internal static void LogError(object data) => Log(LogLevel.Error, data);
-		internal static void LogWarning(object data) => Log(LogLevel.Warning, data);
-		internal static void LogMessage(object data) => Log(LogLevel.Message, data);
-		internal static void LogInfo(object data) => Log(LogLevel.Info, data);
-		internal static void LogDebug(object data) => Log(LogLevel.Debug, data);
-
-		public static ManualLogSource CreateLogSource(string sourceName)
-		{
-			var source = new ManualLogSource(sourceName);
-
-			Sources.Add(source);
-
-			return source;
-		}
-
-
-		private class LogSourceCollection : List<ILogSource>, ICollection<ILogSource>
-		{
-			void ICollection<ILogSource>.Add(ILogSource item)
-			{
-				if (item == null)
-					throw new ArgumentNullException(nameof(item), "Log sources cannot be null when added to the source list.");
-
-				item.LogEvent += InternalLogEvent;
-
-				base.Add(item);
-			}
-
-			void ICollection<ILogSource>.Clear()
-			{
-				foreach (var item in base.ToArray())
-				{
-					((ICollection<ILogSource>)this).Remove(item);
-				}
-			}
-
-			bool ICollection<ILogSource>.Remove(ILogSource item)
-			{
-				if (item == null)
-					return false;
-
-				if (!base.Contains(item))
-					return false;
-
-				item.LogEvent -= InternalLogEvent;
-
-				base.Remove(item);
-
-				return true;
-			}
-		}
-	}
+using System;
+using System.Collections.Generic;
+
+namespace BepInEx.Logging
+{
+	/// <summary>
+	/// A static <see cref="BaseLogger"/> instance.
+	/// </summary>
+	public static class Logger
+	{
+		public static ICollection<ILogListener> Listeners { get; } = new List<ILogListener>();
+
+		public static ICollection<ILogSource> Sources { get; } = new LogSourceCollection();
+
+		private static readonly ManualLogSource InternalLogSource = CreateLogSource("BepInEx");
+
+		internal static void InternalLogEvent(object sender, LogEventArgs eventArgs)
+		{
+			foreach (var listener in Listeners)
+			{
+				listener?.LogEvent(sender, eventArgs);
+			}
+		}
+
+		/// <summary>
+		/// Logs an entry to the current logger instance.
+		/// </summary>
+		/// <param name="level">The level of the entry.</param>
+		/// <param name="entry">The textual value of the entry.</param>
+		internal static void Log(LogLevel level, object data)
+		{
+			InternalLogSource.Log(level, data);
+		}
+
+		internal static void LogFatal(object data) => Log(LogLevel.Fatal, data);
+		internal static void LogError(object data) => Log(LogLevel.Error, data);
+		internal static void LogWarning(object data) => Log(LogLevel.Warning, data);
+		internal static void LogMessage(object data) => Log(LogLevel.Message, data);
+		internal static void LogInfo(object data) => Log(LogLevel.Info, data);
+		internal static void LogDebug(object data) => Log(LogLevel.Debug, data);
+
+		public static ManualLogSource CreateLogSource(string sourceName)
+		{
+			var source = new ManualLogSource(sourceName);
+
+			Sources.Add(source);
+
+			return source;
+		}
+
+
+		private class LogSourceCollection : List<ILogSource>, ICollection<ILogSource>
+		{
+			void ICollection<ILogSource>.Add(ILogSource item)
+			{
+				if (item == null)
+					throw new ArgumentNullException(nameof(item), "Log sources cannot be null when added to the source list.");
+
+				item.LogEvent += InternalLogEvent;
+
+				base.Add(item);
+			}
+
+			void ICollection<ILogSource>.Clear()
+			{
+				foreach (var item in base.ToArray())
+				{
+					((ICollection<ILogSource>)this).Remove(item);
+				}
+			}
+
+			bool ICollection<ILogSource>.Remove(ILogSource item)
+			{
+				if (item == null)
+					return false;
+
+				if (!base.Contains(item))
+					return false;
+
+				item.LogEvent -= InternalLogEvent;
+
+				base.Remove(item);
+
+				return true;
+			}
+		}
+	}
 }

+ 56 - 56
BepInEx/Logging/UnityLogListener.cs

@@ -1,57 +1,57 @@
-using System;
-using System.Reflection;
-using System.Runtime.CompilerServices;
-
-namespace BepInEx.Logging
-{
-	/// <summary>
-	/// Logs entries using Unity specific outputs.
-	/// </summary>
-	public class UnityLogListener : ILogListener
-	{
-		internal static readonly Action<string> WriteStringToUnityLog;
-
-		static UnityLogListener()
-		{
-			foreach (MethodInfo methodInfo in typeof(UnityEngine.UnityLogWriter).GetMethods(BindingFlags.Static | BindingFlags.Public))
-			{
-				try
-				{
-					methodInfo.Invoke(null, new object[] { "" });
-				}
-				catch
-				{
-					continue;
-				}
-
-				WriteStringToUnityLog = (Action<string>)Delegate.CreateDelegate(typeof(Action<string>), methodInfo);
-				break;
-			}
-
-			if (WriteStringToUnityLog == null)
-				Logger.LogError("Unable to start Unity log writer");
-		}
-
-		public void LogEvent(object sender, LogEventArgs eventArgs)
-		{
-			if (eventArgs.Source is UnityLogSource)
-				return;
-
-			WriteStringToUnityLog?.Invoke(eventArgs.ToStringLine());
-		}
-
-		public void Dispose() { }
-	}
-}
-
-namespace UnityEngine
-{
-	internal sealed class UnityLogWriter
-	{
-		[MethodImpl(MethodImplOptions.InternalCall)]
-		public static extern void WriteStringToUnityLogImpl(string s);
-
-		[MethodImpl(MethodImplOptions.InternalCall)]
-		public static extern void WriteStringToUnityLog(string s);
-	}
+using System;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+
+namespace BepInEx.Logging
+{
+	/// <summary>
+	/// Logs entries using Unity specific outputs.
+	/// </summary>
+	public class UnityLogListener : ILogListener
+	{
+		internal static readonly Action<string> WriteStringToUnityLog;
+
+		static UnityLogListener()
+		{
+			foreach (MethodInfo methodInfo in typeof(UnityEngine.UnityLogWriter).GetMethods(BindingFlags.Static | BindingFlags.Public))
+			{
+				try
+				{
+					methodInfo.Invoke(null, new object[] { "" });
+				}
+				catch
+				{
+					continue;
+				}
+
+				WriteStringToUnityLog = (Action<string>)Delegate.CreateDelegate(typeof(Action<string>), methodInfo);
+				break;
+			}
+
+			if (WriteStringToUnityLog == null)
+				Logger.LogError("Unable to start Unity log writer");
+		}
+
+		public void LogEvent(object sender, LogEventArgs eventArgs)
+		{
+			if (eventArgs.Source is UnityLogSource)
+				return;
+
+			WriteStringToUnityLog?.Invoke(eventArgs.ToStringLine());
+		}
+
+		public void Dispose() { }
+	}
+}
+
+namespace UnityEngine
+{
+	internal sealed class UnityLogWriter
+	{
+		[MethodImpl(MethodImplOptions.InternalCall)]
+		public static extern void WriteStringToUnityLogImpl(string s);
+
+		[MethodImpl(MethodImplOptions.InternalCall)]
+		public static extern void WriteStringToUnityLog(string s);
+	}
 }

+ 7 - 1
BepInEx/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.CurrentOs == Platform.MacOS
+				? Utility.ParentDirectory(executablePath, 4)
+				: Path.GetDirectoryName(executablePath);
+
 			ManagedPath = managedPath ?? Utility.CombinePaths(GameRootPath, $"{ProcessName}_Data", "Managed");
 			BepInExRootPath = bepinRootPath ?? Path.Combine(GameRootPath, "BepInEx");
 			ConfigPath = Path.Combine(BepInExRootPath, "config");
@@ -58,6 +63,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; }
 

+ 2 - 2
BepInEx/Properties/AssemblyInfo.cs

@@ -37,5 +37,5 @@ using BepInEx;
 // You can specify all the values or you can default the Build and Revision Numbers
 // by using the '*' as shown below:
 // [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("5.0.1.0")]
-[assembly: AssemblyFileVersion("5.0.1.0")]
+[assembly: AssemblyVersion("5.1.0.0")]
+[assembly: AssemblyFileVersion("5.1.0.0")]

+ 42 - 0
BepInEx/Utility.cs

@@ -5,6 +5,7 @@ using System.Linq;
 using System.Reflection;
 using System.Reflection.Emit;
 using Mono.Cecil;
+using MonoMod.Utils;
 
 namespace BepInEx
 {
@@ -34,6 +35,8 @@ namespace BepInEx
 			{
 				// Suppress ArgumentNullException
 			}
+
+			CheckPlatform();
 		}
 
 		/// <summary>
@@ -65,6 +68,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>
@@ -237,5 +254,30 @@ namespace BepInEx
 				return false;
 			}
 		}
+
+
+
+		// Adapted from https://github.com/MonoMod/MonoMod.Common/blob/master/Utils/PlatformHelper.cs#L13
+		private static void 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;
+
+			CurrentOs = cur;
+		}
+
+		/// <summary>
+		/// Current OS BepInEx is running on.
+		/// </summary>
+		public static Platform CurrentOs { get; private set; }
 	}
 }

+ 1 - 0
BepInEx/packages.config

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

+ 31 - 10
build.cake

@@ -4,7 +4,8 @@
 #addin nuget:?package=Cake.Json&version=4.0.0
 #addin nuget:?package=Newtonsoft.Json&version=11.0.2
 
-const string DOORSTOP_VER = "3.0.0.0";
+const string DOORSTOP_VER_WIN = "3.0.0.0";
+const string DOORSTOP_VER_NIX = "1.2.0.0";
 const string DOORSTOP_DLL = "version.dll";
 
 var target = Argument("target", "Build");
@@ -96,14 +97,22 @@ Task("DownloadDoorstop")
     var doorstopPath = Directory("./bin/doorstop");
     var doorstopX64Path = doorstopPath + File("doorstop_x64.zip");
     var doorstopX86Path = doorstopPath + File("doorstop_x86.zip");
+    var doorstopLinuxPath = doorstopPath + File("doorstop_linux.zip");
+    var doorstopMacPath = doorstopPath + File("doorstop_macos.zip");
     CreateDirectory(doorstopPath);
 
-    DownloadFile($"https://github.com/NeighTools/UnityDoorstop/releases/download/v{DOORSTOP_VER}/Doorstop_x64_{DOORSTOP_VER}.zip", doorstopX64Path);
-    DownloadFile($"https://github.com/NeighTools/UnityDoorstop/releases/download/v{DOORSTOP_VER}/Doorstop_x86_{DOORSTOP_VER}.zip", doorstopX86Path);
+    DownloadFile($"https://github.com/NeighTools/UnityDoorstop/releases/download/v{DOORSTOP_VER_WIN}/Doorstop_x64_{DOORSTOP_VER_WIN}.zip", doorstopX64Path);
+    DownloadFile($"https://github.com/NeighTools/UnityDoorstop/releases/download/v{DOORSTOP_VER_WIN}/Doorstop_x86_{DOORSTOP_VER_WIN}.zip", doorstopX86Path);
+
+    DownloadFile($"https://github.com/NeighTools/UnityDoorstop.Unix/releases/download/v{DOORSTOP_VER_NIX}/doorstop_v{DOORSTOP_VER_NIX}_linux.zip", doorstopLinuxPath);
+    DownloadFile($"https://github.com/NeighTools/UnityDoorstop.Unix/releases/download/v{DOORSTOP_VER_NIX}/doorstop_v{DOORSTOP_VER_NIX}_macos.zip", doorstopMacPath);
 
     Information("Extracting Doorstop");
     ZipUncompress(doorstopX86Path, doorstopPath + Directory("x86"));
     ZipUncompress(doorstopX64Path, doorstopPath + Directory("x64"));
+
+    ZipUncompress(doorstopLinuxPath, doorstopPath + Directory("nix"));
+    ZipUncompress(doorstopMacPath, doorstopPath + Directory("nix"));
 });
 
 Task("MakeDist")
@@ -124,25 +133,32 @@ Task("MakeDist")
                         .WithToken("commit_log", RunGit($"--no-pager log --no-merges --pretty=\"format:* (%h) [%an] %s\" {latestTag}..HEAD", "\r\n"))
                         .ToString();
 
-    void PackageBepin(string arch) 
+    void PackageBepin(string arch, string copyPattern, string doorstopConfigPattern, string libDir = null, bool ensureLf = false) 
     {
         var distArchDir = distDir + Directory(arch);
-        var doorstopArchPath = doorstopPath + Directory(arch) + File(DOORSTOP_DLL);
         var bepinDir = distArchDir + Directory("BepInEx");
-        
+        var doorstopTargetDir = distArchDir;
+        if(libDir != null) doorstopTargetDir += Directory(libDir);
+
+        var doorstopFiles = doorstopPath + Directory(arch) + File(copyPattern);
+
         CreateDirectory(distArchDir);
+        CreateDirectory(doorstopTargetDir);
         CreateDirectory(bepinDir + Directory("core"));
         CreateDirectory(bepinDir + Directory("plugins"));
         CreateDirectory(bepinDir + Directory("patchers"));
 
-        CopyFiles("./doorstop/*.*", distArchDir);
+        CopyFiles($"./doorstop/{doorstopConfigPattern}", distArchDir);
+        if(ensureLf)
+            ReplaceTextInFiles($"{distArchDir}/{doorstopConfigPattern}", "\r\n", "\n");
         CopyFiles("./bin/*.*", bepinDir + Directory("core"));
-        CopyFileToDirectory(doorstopArchPath, distArchDir);
+        CopyFiles(doorstopFiles, doorstopTargetDir);
         FileWriteText(distArchDir + File("changelog.txt"), changelog);
     }
 
-    PackageBepin("x86");
-    PackageBepin("x64");
+    PackageBepin("x86", DOORSTOP_DLL, "doorstop_config.ini");
+    PackageBepin("x64", DOORSTOP_DLL, "doorstop_config.ini");
+    PackageBepin("nix", "*.*", "run_bepinex.sh", "doorstop_libs", true);
     CopyFileToDirectory(File("./bin/patcher/BepInEx.Patcher.exe"), distPatcherDir);
 });
 
@@ -156,6 +172,7 @@ Task("Pack")
     Information("Packing BepInEx");
     ZipCompress(distDir + Directory("x86"), distDir + File($"BepInEx_x86{commitPrefix}{buildVersion}.zip"));
     ZipCompress(distDir + Directory("x64"), distDir + File($"BepInEx_x64{commitPrefix}{buildVersion}.zip"));
+    ZipCompress(distDir + Directory("nix"), distDir + File($"BepInEx_unix{commitPrefix}{buildVersion}.zip"));
 
     Information("Packing BepInEx.Patcher");
     ZipCompress(distDir + Directory("patcher"), distDir + File($"BepInEx_Patcher{commitPrefix}{buildVersion}.zip"));
@@ -186,6 +203,10 @@ Task("Pack")
                         ["description"] = "BepInEx for x86 machines"
                     },
                     new Dictionary<string, object> {
+                        ["file"] = $"BepInEx_unix{commitPrefix}{buildVersion}.zip",
+                        ["description"] = "BepInEx for Unix with GCC (Linux, MacOS)"
+                    },
+                    new Dictionary<string, object> {
                         ["file"] = $"BepInEx_Patcher{commitPrefix}{buildVersion}.zip",
                         ["description"] = "Hardpatcher for BepInEx. IMPORTANT: USE ONLY IF DOORSTOP DOES NOT WORK FOR SOME REASON!"
                     }

+ 79 - 0
doorstop/run_bepinex.sh

@@ -0,0 +1,79 @@
+#!/bin/sh
+# BepInEx running script
+#
+# This script is used to run a Unity game with BepInEx enabled.
+#
+# Usage: Configure the script below and simply run this script when you want to run your game modded.
+
+if [ -z "$1" ]; then echo "Please open run.sh in a text editor and configure executable name. Comment or remove this line when you're done." && exit 1; fi
+
+# -------- SETTINGS --------
+# ---- EDIT AS NEEDED ------
+
+# EDIT THIS: The name of the executable to run
+# LINUX: This is the name of the Unity game executable 
+# MACOS: This is the name of the game app WITHOUT the .app
+executable_name="";
+
+
+# The rest is automatically handled by BepInEx
+
+# Whether or not to enable Doorstop. Valid values: TRUE or FALSE
+export DOORSTOP_ENABLE=TRUE;
+
+# What .NET assembly to execute. Valid value is a path to a .NET DLL that mono can execute.
+export DOORSTOP_INVOKE_DLL_PATH=${PWD}/BepInEx/core/BepInEx.Preloader.dll;
+
+
+# ----- DO NOT EDIT FROM THIS LINE FORWARD  ------
+# ----- (unless you know what you're doing) ------
+
+# Backup current LD_PRELOAD because it can break `file` when running from Steam
+LD_PRELOAD_BAK=$LD_PRELOAD;
+export LD_PRELOAD="";
+DYLD_INSERT_LIBRARIES_BAK=$DYLD_INSERT_LIBRARIES;
+export DYLD_INSERT_LIBRARIES="";
+
+doorstop_libs=${PWD}/doorstop_libs;
+arch="";
+executable_path="";
+lib_postfix="";
+
+os_type=`uname -s`;
+case $os_type in
+    Linux*)     executable_path=${PWD}/${executable_name};
+                lib_postfix="so";;
+    Darwin*)    executable_path=${PWD}/${executable_name}.app/Contents/MacOS/${executable_name};
+                lib_postfix="dylib";;
+    *)          echo "Cannot identify OS (got $(uname -s))!";
+                echo "Please create an issue at https://github.com/BepInEx/BepInEx/issues."; 
+                exit 1;;
+esac
+
+# Special case: if there is an arg, use that as executable path
+# Linux: arg is path to the executable
+# MacOS: arg is path to the .app folder which we need to resolve to the exectuable
+if [ -n "$1" ]; then
+    case $os_type in
+        Linux*)     executable_path=$1;;
+        Darwin*)    executable_name=`basename "$1" .app`;
+                    executable_path=$1/Contents/MacOS/$executable_name;;
+    esac
+fi
+
+executable_type=`file -b "${executable_path}"`;
+case $executable_type in
+    *64-bit*)           arch="x64";;
+    *32-bit*|*i386*)    arch="x86";;
+    *)          echo "Cannot identify executable type (got ${executable_type})!"; 
+                echo "Please create an issue at https://github.com/BepInEx/BepInEx/issues."; 
+                exit 1;;
+esac
+
+doorstop_libname=libdoorstop_${arch}.${lib_postfix};
+export LD_LIBRARY_PATH=${doorstop_libs}:${LD_LIBRARY_PATH};
+export LD_PRELOAD=${doorstop_libs}/$doorstop_libname:$LD_PRELOAD_BAK;
+export DYLD_LIBRARY_PATH=${doorstop_libs}:${DYLD_LIBRARY_PATH};
+export DYLD_INSERT_LIBRARIES=${doorstop_libs}/$doorstop_libname:$DYLD_INSERT_LIBRARIES_BAK;
+
+"${executable_path}"