Przeglądaj źródła

Implement manual xterm escape codes for Unity 4.x

Bepis 4 lat temu
rodzic
commit
8d1df72a0b

+ 7 - 7
BepInEx.Preloader/RuntimeFixes/XTermFix.cs

@@ -13,6 +13,12 @@ namespace BepInEx.Preloader.RuntimeFixes
 			if (Environment.OSVersion.Platform != PlatformID.Unix)
 				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
@@ -43,9 +49,6 @@ namespace BepInEx.Preloader.RuntimeFixes
 			int b3 = buffer[offset + 2];
 			int b4 = buffer[offset + 3];
 
-			if (b1 == 255 && b2 == 255 && b3 == 255 && b4 == 255)
-				return -1;
-
 			return b1 | (b2 << 8) | (b3 << 16) | (b4 << 24);
 		}
 
@@ -54,9 +57,6 @@ namespace BepInEx.Preloader.RuntimeFixes
 			int b1 = buffer[offset];
 			int b2 = buffer[offset + 1];
 
-			if (b1 == 255 && b2 == 255)
-				return -1;
-
 			return (short)(b1 | (b2 << 8));
 		}
 
@@ -77,7 +77,7 @@ namespace BepInEx.Preloader.RuntimeFixes
 				throw new Exception($"Unknown xterm header format: {magic}");
 		}
 
-		public static bool ReadHeaderPrefix(object __instance, byte[] buffer, ref int position, ref short ___boolSize, ref short ___numSize, ref short ___strOffsets)
+		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;

+ 1 - 0
BepInEx/BepInEx.csproj

@@ -72,6 +72,7 @@
     <Compile Include="Console\Unix\ConsoleWriter.cs" />
     <Compile Include="Console\Unix\DynDllImport.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" />

+ 35 - 8
BepInEx/Console/SafeConsole.cs

@@ -9,16 +9,14 @@ using System.Reflection;
 namespace UnityInjector.ConsoleUtil
 {
 	/// <summary>
-	/// Console class required for Unity 4.x, which do not have ForegroundColor and BackgroundColor properties
+	/// 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 bool BackgroundColorExists { get; private set; }
 
 		public static ConsoleColor BackgroundColor
 		{
@@ -28,12 +26,26 @@ namespace UnityInjector.ConsoleUtil
 
 		public static bool ForegroundColorExists { get; private set; }
 
+		private static GetColorDelegate _getForegroundColor;
+		private static SetColorDelegate _setForegroundColor;
+
 		public static ConsoleColor ForegroundColor
 		{
 			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()
 		{
 			var tConsole = typeof(Console);
@@ -44,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)
@@ -65,12 +81,23 @@ namespace UnityInjector.ConsoleUtil
 				? (GetColorDelegate)Delegate.CreateDelegate(typeof(GetColorDelegate), gbc)
 				: (() => ConsoleColor.Black);
 
+			_getTitle = gtt != null
+				? (GetStringDelegate)Delegate.CreateDelegate(typeof(GetColorDelegate), gtt)
+				: (() => string.Empty);
+
+			_setTitle = stt != null
+				? (SetStringDelegate)Delegate.CreateDelegate(typeof(GetColorDelegate), 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);
 	}
 }

+ 35 - 7
BepInEx/Console/Unix/LinuxConsoleDriver.cs

@@ -18,19 +18,40 @@ namespace BepInEx.Unix
 		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;
 
 			var duplicateStream = UnixStreamHelper.CreateDuplicateStream(1);
 
-			var writer = ConsoleWriter.CreateConsoleStreamWriter(duplicateStream, Console.Out.Encoding, true);
+			if (UseMonoTtyDriver)
+			{
+				// 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);
+				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);
+				TtyInfo = TtyHandler.GetTtyInfo();
+			}
 
 			ConsoleOut = StandardOut;
 		}
@@ -47,14 +68,14 @@ namespace BepInEx.Unix
 
 		public void SetConsoleColor(ConsoleColor color)
 		{
-			if (SafeConsole.ForegroundColorExists)
+			if (UseMonoTtyDriver)
 			{
 				// Use mono's inbuilt terminfo driver to set the foreground color for us
 				SafeConsole.ForegroundColor = color;
 			}
 			else
 			{
-				throw new PlatformNotSupportedException("Cannot set Unix TTY color as mono implementation is missing");
+				ConsoleOut.Write(TtyInfo.GetAnsiCode(color));
 			}
 		}
 
@@ -65,7 +86,14 @@ namespace BepInEx.Unix
 
 		public void SetConsoleTitle(string title)
 		{
-			Console.Title = title;
+			if (UseMonoTtyDriver)
+			{
+				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
+		}
+	}
+}