Browse Source

Implement cross-platform improvements from master
Contains fixes rebased from d2afe06 ... 9f4dcb6

Bepis 4 years ago
parent
commit
1c32115fa2
34 changed files with 1117 additions and 257 deletions
  1. 18 7
      BepInEx.Core/BepInEx.Core.csproj
  2. 7 1
      BepInEx.Core/Bootstrap/BaseChainloader.cs
  3. 127 0
      BepInEx.Core/Console/ConsoleManager.cs
  4. 26 0
      BepInEx.Core/Console/IConsoleDriver.cs
  5. 46 9
      BepInEx.Core/ConsoleUtil/SafeConsole.cs
  6. 57 0
      BepInEx.Core/Console/Unix/ConsoleWriter.cs
  7. 112 0
      BepInEx.Core/Console/Unix/LinuxConsoleDriver.cs
  8. 214 0
      BepInEx.Core/Console/Unix/TtyHandler.cs
  9. 85 0
      BepInEx.Core/Console/Unix/UnixStream.cs
  10. 60 0
      BepInEx.Core/Console/Unix/UnixStreamHelper.cs
  11. 0 0
      BepInEx.Core/Console/Windows/ConsoleEncoding/ConsoleEncoding.Buffers.cs
  12. 0 0
      BepInEx.Core/Console/Windows/ConsoleEncoding/ConsoleEncoding.PInvoke.cs
  13. 2 0
      BepInEx.Core/ConsoleUtil/ConsoleEncoding/ConsoleEncoding.cs
  14. 16 38
      BepInEx.Core/ConsoleUtil/ConsoleWindow.cs
  15. 11 19
      BepInEx.Core/ConsoleUtil/Kon.cs
  16. 114 0
      BepInEx.Core/Console/Windows/WindowsConsoleDriver.cs
  17. 0 119
      BepInEx.Core/ConsoleManager.cs
  18. 2 4
      BepInEx.Core/Logging/ConsoleLogListener.cs
  19. 2 2
      BepInEx.Core/Logging/DiskLogListener.cs
  20. 10 0
      BepInEx.Core/Logging/LogEventArgs.cs
  21. 1 1
      BepInEx.Core/Logging/Logger.cs
  22. 24 0
      BepInEx.Core/Utility.cs
  23. 2 2
      BepInEx.IL2CPP/BepInEx.IL2CPP.csproj
  24. 1 1
      BepInEx.NetLauncher/BepInEx.NetLauncher.csproj
  25. 1 1
      BepInEx.Preloader.Core/BepInEx.Preloader.Core.csproj
  26. 5 4
      BepInEx.Preloader.Core/Logging/ChainloaderLogHelper.cs
  27. 1 8
      BepInEx.Preloader.Core/Logging/PreloaderLogWriter.cs
  28. 2 1
      BepInEx.Preloader.Unity/BepInEx.Preloader.Unity.csproj
  29. 11 2
      BepInEx.Preloader.Unity/DoorstopEntrypoint.cs
  30. 136 0
      BepInEx.Preloader.Unity/RuntimeFixes/XTermFix.cs
  31. 13 10
      BepInEx.Preloader.Unity/UnityPreloader.cs
  32. 1 1
      BepInEx.Unity/BepInEx.Unity.csproj
  33. 8 23
      BepInEx.Unity/Bootstrap/UnityChainloader.cs
  34. 2 4
      BepInEx.Unity/Logging/UnityLogListener.cs

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

@@ -47,7 +47,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,12 +70,6 @@
     <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\LogEventArgs.cs" />
     <Compile Include="Logging\Logger.cs" />
@@ -82,6 +89,10 @@
     <PackageReference Include="Mono.Cecil">
       <Version>0.10.4</Version>
     </PackageReference>
+    <PackageReference Include="MonoMod.Utils">
+      <Version>20.8.3.5</Version>
+    </PackageReference>
   </ItemGroup>
+  <ItemGroup />
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
 </Project>

+ 7 - 1
BepInEx.Core/Bootstrap/BaseChainloader.cs

@@ -15,7 +15,7 @@ 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} - {Path.GetFileNameWithoutExtension(Process.GetCurrentProcess().ProcessName)}";
 
 		private bool _initialized = false;
 
@@ -228,6 +228,12 @@ namespace BepInEx.Bootstrap
 			}
 			catch (Exception ex)
 			{
+				try
+				{
+					ConsoleManager.CreateConsole();
+				}
+				catch { }
+
 				Logger.LogError("Error occurred starting the game");
 				Logger.LogDebug(ex);
 			}

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

@@ -0,0 +1,127 @@
+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)
+			{
+				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();
+			
+			// 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);
+		}
+
+		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.");
-	}
-}

+ 2 - 4
BepInEx.Core/Logging/ConsoleLogListener.cs

@@ -10,13 +10,11 @@ namespace BepInEx.Logging
 	{
 		public void LogEvent(object sender, LogEventArgs eventArgs)
 		{
-			if ((eventArgs.Level & ConfigConsoleDisplayedLevel.Value) > 0)
+			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);
 		}
 

+ 2 - 2
BepInEx.Core/Logging/DiskLogListener.cs

@@ -51,10 +51,10 @@ namespace BepInEx.Logging
 			if (BlacklistedSources.Contains(eventArgs.Source.SourceName))
 				return;
 
-			if ((eventArgs.Level & DisplayedLogLevel) > 0)
+			if ((eventArgs.Level & DisplayedLogLevel) == 0)
 				return;
 
-			LogWriter.WriteLine($"[{eventArgs.Level,-7}:{((ILogSource)sender).SourceName,10}] {eventArgs.Data}");
+			LogWriter.WriteLine(eventArgs.ToString());
 		}
 
 		public void Dispose()

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

@@ -16,5 +16,15 @@ namespace BepInEx.Logging
 			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}";
+		}
 	}
 }

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

@@ -14,7 +14,7 @@ namespace BepInEx.Logging
 
 		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)
 			{

+ 24 - 0
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
 {
@@ -270,5 +271,28 @@ namespace BepInEx
 
 			return builder.ToString();
 		}
+
+		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

@@ -92,13 +92,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.8.3.5</Version>
     </PackageReference>
   </ItemGroup>
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />

+ 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" />

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

@@ -52,7 +52,7 @@
   </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>

+ 5 - 4
BepInEx.Preloader.Core/Logging/ChainloaderLogHelper.cs

@@ -1,4 +1,5 @@
 using System.Diagnostics;
+using System.IO;
 using System.Linq;
 using BepInEx.Logging;
 
@@ -8,11 +9,11 @@ namespace BepInEx.Preloader.Core.Logging
 	{
 		public static void PrintLogInfo(ManualLogSource log)
 		{
-			string consoleTile = $"BepInEx {typeof(Paths).Assembly.GetName().Version} - {Process.GetCurrentProcess().ProcessName}";
-			log.LogMessage(consoleTile);
+			string consoleTitle = $"BepInEx {typeof(Paths).Assembly.GetName().Version} - {Path.GetFileNameWithoutExtension(Process.GetCurrentProcess().ProcessName)}";
+			log.LogMessage(consoleTitle);
 
 			if (ConsoleManager.ConsoleActive)
-				ConsoleManager.SetConsoleTitle(consoleTile);
+				ConsoleManager.SetConsoleTitle(consoleTitle);
 
 			//See BuildInfoAttribute for more information about this section.
 			object[] attributes = typeof(BuildInfoAttribute).Assembly.GetCustomAttributes(typeof(BuildInfoAttribute), false);
@@ -37,7 +38,7 @@ namespace BepInEx.Preloader.Core.Logging
 
 			foreach (var preloaderLogEvent in PreloaderConsoleListener.LogEvents)
 			{
-				PreloaderLogger.Log.Log(preloaderLogEvent.Level, $"[{ preloaderLogEvent.Source.SourceName,10}] { preloaderLogEvent.Data}");
+				Logger.InternalLogEvent(PreloaderLogger.Log, preloaderLogEvent);
 			}
 
 			if (logListener != null)

+ 1 - 8
BepInEx.Preloader.Core/Logging/PreloaderLogWriter.cs

@@ -9,7 +9,6 @@ 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; }
@@ -32,12 +31,8 @@ namespace BepInEx.Preloader.Core.Logging
 		{
 			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);
+			ConsoleDirectWrite(eventArgs.ToStringLine());
 			ConsoleManager.SetConsoleColor(ConsoleColor.Gray);
 		}
 
@@ -51,8 +46,6 @@ namespace BepInEx.Preloader.Core.Logging
 			StandardOut.WriteLine(value);
 		}
 
-		public override string ToString() => LogBuilder.ToString();
-
 		public void Dispose()
 		{
 			if (LoggerSource != null)

+ 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" />

+ 11 - 2
BepInEx.Preloader.Unity/DoorstopEntrypoint.cs

@@ -2,18 +2,27 @@
 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)
+		public static void PreloaderPreMain(string[] args)
 		{
 			string bepinPath = Path.GetDirectoryName(Path.GetDirectoryName(Path.GetFullPath(EnvVars.DOORSTOP_INVOKE_DLL_PATH)));
 
 			Paths.SetExecutablePath(args[0], bepinPath);
 			AppDomain.CurrentDomain.AssemblyResolve += LocalResolve;
 
+			PreloaderMain(args);
+		}
+
+		private static void PreloaderMain(string[] args)
+		{
+			if (UnityPreloader.ConfigApplyRuntimePatches.Value)
+				XTermFix.Apply();
+
 			UnityPreloader.Run(EnvVars.DOORSTOP_MANAGED_FOLDER_DIR);
 		}
 
@@ -66,7 +75,7 @@ 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))
+								  ?.GetMethod(nameof(UnityPreloaderRunner.PreloaderPreMain))
 								  ?.Invoke(null, new object[] { args });
 
 				AppDomain.CurrentDomain.AssemblyResolve -= ResolveCurrentDirectory;

+ 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;
+		}
+	}
+}

+ 13 - 10
BepInEx.Preloader.Unity/UnityPreloader.cs

@@ -15,6 +15,7 @@ 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,6 +40,7 @@ namespace BepInEx.Preloader.Unity
 		{
 			try
 			{
+				ConsoleManager.Initialize(false);
 				AllocateConsole();
 
 				if (managedDirectory != null)
@@ -51,7 +53,7 @@ namespace BepInEx.Preloader.Unity
 				}, out var harmonyBridgeException);
 
 				Exception runtimePatchException = null;
-				if(bridgeInitialized)
+				if (bridgeInitialized)
 					Utility.TryDo(() =>
 					{
 						if (ConfigApplyRuntimePatches.Value)
@@ -67,7 +69,7 @@ namespace BepInEx.Preloader.Unity
 
 				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}");
 
@@ -234,13 +236,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 +244,14 @@ 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)";
+		}
+
 		#region Config
 
 		private static readonly ConfigEntry<string> ConfigEntrypointAssembly = ConfigFile.CoreConfig.Bind(
@@ -266,7 +269,7 @@ 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.");

+ 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" />

+ 8 - 23
BepInEx.Unity/Bootstrap/UnityChainloader.cs

@@ -1,15 +1,12 @@
 using BepInEx.Configuration;
 using BepInEx.Logging;
-using System;
 using System.Diagnostics;
 using System.IO;
-using System.Linq;
 using System.Reflection;
-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;
 
@@ -45,30 +42,13 @@ 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();
-
-				if (!Logger.Listeners.Any(x => x is ConsoleLogListener))
-					Logger.Listeners.Add(new ConsoleLogListener());
-			}
-
-			// Fix for standard output getting overwritten by UnityLogger
-			if (ConsoleManager.StandardOut != null)
-			{
-				Console.SetOut(ConsoleManager.StandardOut);
-
-				var encoding = ConsoleManager.ConfigConsoleShiftJis.Value ? 932 : (uint)Encoding.UTF8.CodePage;
-				ConsoleManager.SetConsoleEncoding(encoding);
-			}
-
 			Logger.Listeners.Add(new UnityLogListener());
 
 			if (ConfigUnityLogging.Value)
@@ -78,12 +58,17 @@ namespace BepInEx.Unity.Bootstrap
 			base.InitializeLoggers();
 
 
+			if (Utility.CurrentPlatform != Platform.Windows)
+			{
+				Logger.LogInfo($"Detected Unity version: v{Application.unityVersion}");
+			}
+
+
 			if (!ConfigDiskWriteUnityLog.Value)
 			{
 				DiskLogListener.BlacklistedSources.Add("Unity Log");
 			}
 
-
 			ChainloaderLogHelper.RewritePreloaderLogs();
 		}
 

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

@@ -35,12 +35,10 @@ namespace BepInEx.Unity.Logging
 
 		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);
+			WriteStringToUnityLog?.Invoke(eventArgs.ToStringLine());
 		}
 
 		public void Dispose() { }