diff --git a/BuildTools b/BuildTools index de919c49..af3be92b 160000 --- a/BuildTools +++ b/BuildTools @@ -1 +1 @@ -Subproject commit de919c49496a7e7e11382bd876473018ddceaa1a +Subproject commit af3be92b829e0068671666d74b25fb7f2ec4b520 diff --git a/Doorstop b/Doorstop index 6b6de3b8..642c22e3 160000 --- a/Doorstop +++ b/Doorstop @@ -1 +1 @@ -Subproject commit 6b6de3b81cf142c73a8ef845f705fb1e51a7670d +Subproject commit 642c22e38a76bbdaa46dbb3aaa565c354a2ef68e diff --git a/IPA.Injector/IPA.Injector.csproj b/IPA.Injector/IPA.Injector.csproj index 14e11358..c7cc4189 100644 --- a/IPA.Injector/IPA.Injector.csproj +++ b/IPA.Injector/IPA.Injector.csproj @@ -14,7 +14,6 @@ true $(SolutionDir)=C:\ portable - true diff --git a/IPA.Injector/Injector.cs b/IPA.Injector/Injector.cs index fae10e9f..0649a4f6 100644 --- a/IPA.Injector/Injector.cs +++ b/IPA.Injector/Injector.cs @@ -239,7 +239,7 @@ namespace IPA.Injector }; // need to reinit streams singe Unity seems to redirect stdout - WinConsole.InitializeStreams(); + StdoutInterceptor.RedirectConsole(); var bootstrapper = new GameObject("NonDestructiveBootstrapper").AddComponent(); bootstrapper.Destroyed += Bootstrapper_Destroyed; diff --git a/IPA.Loader/IPA.Loader.csproj b/IPA.Loader/IPA.Loader.csproj index b839eea7..73f38bb1 100644 --- a/IPA.Loader/IPA.Loader.csproj +++ b/IPA.Loader/IPA.Loader.csproj @@ -14,7 +14,6 @@ true $(SolutionDir)=C:\ portable - true @@ -39,6 +38,9 @@ bin\Release\IPA.Loader.xml + + ..\Libs\0Harmony.dll + @@ -76,6 +78,7 @@ + diff --git a/IPA.Loader/Logging/ConsoleWindow.cs b/IPA.Loader/Logging/ConsoleWindow.cs index f8e655ae..d9822106 100644 --- a/IPA.Loader/Logging/ConsoleWindow.cs +++ b/IPA.Loader/Logging/ConsoleWindow.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.IO; using System.Runtime.InteropServices; using Microsoft.Win32.SafeHandles; @@ -51,6 +52,16 @@ namespace IPA.Logging ConOut = writer; Console.SetOut(writer); Console.SetError(writer); + + var handle = GetStdHandle(-11); // get stdout handle (should be CONOUT$ at this point) + if (GetConsoleMode(handle, out var mode)) + { + mode |= EnableVTProcessing; + if (!SetConsoleMode(handle, mode)) + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + else + throw new Win32Exception(Marshal.GetLastWin32Error()); } } @@ -108,6 +119,17 @@ namespace IPA.Logging IntPtr hTemplateFile ); + [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] + private static extern bool GetConsoleMode(IntPtr hConsoleHandle, out uint lpMode); + + [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] + private static extern bool SetConsoleMode(IntPtr hConsoleHandle, uint dwMode); + + [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] + private static extern IntPtr GetStdHandle(int nStdHandle); + + private const uint EnableVTProcessing = 0x0004; + private const uint GenericWrite = 0x40000000; private const uint GenericRead = 0x80000000; private const uint FileShareRead = 0x00000001; diff --git a/IPA.Loader/Logging/Logger.cs b/IPA.Loader/Logging/Logger.cs index 51438faf..8565bcd1 100644 --- a/IPA.Loader/Logging/Logger.cs +++ b/IPA.Loader/Logging/Logger.cs @@ -21,6 +21,18 @@ namespace IPA.Logging } } + private static StandardLogger _stdout; + + internal static StandardLogger stdout + { + get + { + if (_stdout == null) + _stdout = new StandardLogger("_"); + return _stdout; + } + } + internal static Logger updater => log.GetChildLogger("Updater"); internal static Logger libLoader => log.GetChildLogger("LibraryLoader"); internal static Logger loader => log.GetChildLogger("Loader"); diff --git a/IPA.Loader/Logging/Printers/ColorlessConsolePrinter.cs b/IPA.Loader/Logging/Printers/ColorlessConsolePrinter.cs index 754b75ed..de346b70 100644 --- a/IPA.Loader/Logging/Printers/ColorlessConsolePrinter.cs +++ b/IPA.Loader/Logging/Printers/ColorlessConsolePrinter.cs @@ -1,8 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace IPA.Logging.Printers { diff --git a/IPA.Loader/Logging/Printers/GZFilePrinter.cs b/IPA.Loader/Logging/Printers/GZFilePrinter.cs index 5f39d9cc..94f095e7 100644 --- a/IPA.Loader/Logging/Printers/GZFilePrinter.cs +++ b/IPA.Loader/Logging/Printers/GZFilePrinter.cs @@ -3,6 +3,7 @@ using System; using System.IO; using System.Runtime.InteropServices; using System.Text; +using System.Text.RegularExpressions; namespace IPA.Logging.Printers { @@ -18,6 +19,8 @@ namespace IPA.Logging.Printers IntPtr lpSecurityAttributes ); + internal static Regex removeControlCodes = new Regex("\x1b\\[\\d+m", RegexOptions.Compiled); + private FileInfo fileInfo; /// @@ -45,7 +48,7 @@ namespace IPA.Logging.Printers fileInfo = new FileInfo(fileInfo.FullName + ".gz"); fileInfo.Create().Close(); - var symlink = new FileInfo(Path.Combine(fileInfo.DirectoryName ?? throw new InvalidOperationException(), $"latest{ext}.gz")); + var symlink = new FileInfo(Path.Combine(fileInfo.DirectoryName ?? throw new InvalidOperationException(), $"_latest{ext}.gz")); if (symlink.Exists) symlink.Delete(); try diff --git a/IPA.Loader/Logging/Printers/GlobalLogFilePrinter.cs b/IPA.Loader/Logging/Printers/GlobalLogFilePrinter.cs index 59329a54..009f3e92 100644 --- a/IPA.Loader/Logging/Printers/GlobalLogFilePrinter.cs +++ b/IPA.Loader/Logging/Printers/GlobalLogFilePrinter.cs @@ -22,7 +22,7 @@ namespace IPA.Logging.Printers /// the message to print public override void Print(Logger.Level level, DateTime time, string logName, string message) { - foreach (var line in message.Split(new[] { "\n", Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries)) + foreach (var line in removeControlCodes.Replace(message, "").Split(new[] { "\n", Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries)) FileWriter.WriteLine(Logger.LogFormat, line, logName, time, level.ToString().ToUpper()); } diff --git a/IPA.Loader/Logging/Printers/PluginLogFilePrinter.cs b/IPA.Loader/Logging/Printers/PluginLogFilePrinter.cs index eda96b21..f7d2770f 100644 --- a/IPA.Loader/Logging/Printers/PluginLogFilePrinter.cs +++ b/IPA.Loader/Logging/Printers/PluginLogFilePrinter.cs @@ -45,7 +45,7 @@ namespace IPA.Logging.Printers /// the message to print public override void Print(Logger.Level level, DateTime time, string logName, string message) { - foreach (var line in message.Split(new[] { "\n", Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries)) + foreach (var line in removeControlCodes.Replace(message, "").Split(new[] { "\n", Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries)) FileWriter.WriteLine(Logger.LogFormat, line, logName, time, level.ToString().ToUpper()); } } diff --git a/IPA.Loader/Logging/Printers/PluginSubLogPrinter.cs b/IPA.Loader/Logging/Printers/PluginSubLogPrinter.cs index 54bdead2..5446f46f 100644 --- a/IPA.Loader/Logging/Printers/PluginSubLogPrinter.cs +++ b/IPA.Loader/Logging/Printers/PluginSubLogPrinter.cs @@ -48,7 +48,7 @@ namespace IPA.Logging.Printers /// the message to print public override void Print(Logger.Level level, DateTime time, string logName, string message) { - foreach (var line in message.Split(new[] { "\n", Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries)) + foreach (var line in removeControlCodes.Replace(message, "").Split(new[] { "\n", Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries)) FileWriter.WriteLine("[{2} @ {1:HH:mm:ss}] {0}", line, time, level.ToString().ToUpper()); } } diff --git a/IPA.Loader/Logging/StandardLogger.cs b/IPA.Loader/Logging/StandardLogger.cs index 197c3ac9..1672bcfb 100644 --- a/IPA.Loader/Logging/StandardLogger.cs +++ b/IPA.Loader/Logging/StandardLogger.cs @@ -71,6 +71,7 @@ namespace IPA.Logging Color = ConsoleColor.Magenta, } }); + addedConsolePrinters = true; } } @@ -135,7 +136,8 @@ namespace IPA.Logging { if (!addedConsolePrinters) AddDefaultPrinter(new ColorlessConsolePrinter()); - + + StdoutInterceptor.Intercept(); finalizedDefaultPrinters = true; } diff --git a/IPA.Loader/Logging/StdoutInterceptor.cs b/IPA.Loader/Logging/StdoutInterceptor.cs new file mode 100644 index 00000000..88693ce3 --- /dev/null +++ b/IPA.Loader/Logging/StdoutInterceptor.cs @@ -0,0 +1,190 @@ +using Harmony; +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection.Emit; +using System.Text; + +namespace IPA.Logging +{ + internal class StdoutInterceptor : TextWriter + { + public override Encoding Encoding => Encoding.Default; + + private bool isStdErr; + + public override void Write(char value) + { + Write(value.ToString()); + } + + private string lineBuffer = ""; + + public override void Write(string value) + { + lineBuffer += value; + + var parts = lineBuffer.Split(new[] { Environment.NewLine, "\n", "\r" }, StringSplitOptions.None); + for (int i = 0; i < parts.Length; i++) + { + if (i + 1 == parts.Length) // last element + lineBuffer = parts[i]; + else + { + var str = parts[i]; + if (string.IsNullOrEmpty(str)) continue; + if (!isStdErr && WinConsole.IsInitialized) + str = ConsoleColorToForegroundSet(currentColor) + str; + + if (isStdErr) + Logger.stdout.Error(str); + else + Logger.stdout.Info(str); + } + + } + } + + private const ConsoleColor defaultColor = ConsoleColor.Gray; + private ConsoleColor currentColor = defaultColor; + + private static string ConsoleColorToForegroundSet(ConsoleColor col) + { + string code = "0"; // reset + + switch (col) + { + case ConsoleColor.Black: + code = "30"; + break; + case ConsoleColor.DarkBlue: + code = "34"; + break; + case ConsoleColor.DarkGreen: + code = "32"; + break; + case ConsoleColor.DarkCyan: + code = "36"; + break; + case ConsoleColor.DarkRed: + code = "31"; + break; + case ConsoleColor.DarkMagenta: + code = "35"; + break; + case ConsoleColor.DarkYellow: + code = "33"; + break; + case ConsoleColor.Gray: + code = "37"; + break; + case ConsoleColor.DarkGray: + code = "90"; // literally bright black + break; + case ConsoleColor.Blue: + code = "94"; + break; + case ConsoleColor.Green: + code = "92"; + break; + case ConsoleColor.Cyan: + code = "96"; + break; + case ConsoleColor.Red: + code = "91"; + break; + case ConsoleColor.Magenta: + code = "95"; + break; + case ConsoleColor.Yellow: + code = "93"; + break; + case ConsoleColor.White: + code = "97"; + break; + } + + return "\x1b[" + code + "m"; + } + + private static StdoutInterceptor stdoutInterceptor; + private static StdoutInterceptor stderrInterceptor; + + private static class ConsoleHarmonyPatches + { + public static void Patch(HarmonyInstance harmony) + { + var console = typeof(Console); + var resetColor = console.GetMethod("ResetColor"); + var foregroundProperty = console.GetProperty("ForegroundColor"); + var setFg = foregroundProperty.SetMethod; + var getFg = foregroundProperty.GetMethod; + + harmony.Patch(resetColor, transpiler: new HarmonyMethod(typeof(ConsoleHarmonyPatches), "PatchResetColor")); + harmony.Patch(setFg, transpiler: new HarmonyMethod(typeof(ConsoleHarmonyPatches), "PatchSetForegroundColor")); + harmony.Patch(getFg, transpiler: new HarmonyMethod(typeof(ConsoleHarmonyPatches), "PatchGetForegroundColor")); + } + + public static ConsoleColor GetColor() => stdoutInterceptor.currentColor; + public static void SetColor(ConsoleColor col) => stdoutInterceptor.currentColor = col; + public static void ResetColor() => stdoutInterceptor.currentColor = defaultColor; + + public static IEnumerable PatchGetForegroundColor(IEnumerable _) + { + var getColorM = typeof(ConsoleHarmonyPatches).GetMethod("GetColor"); + return new[] { + new CodeInstruction(OpCodes.Call, getColorM), + new CodeInstruction(OpCodes.Ret) + }; + } + + public static IEnumerable PatchSetForegroundColor(IEnumerable _) + { + var setColorM = typeof(ConsoleHarmonyPatches).GetMethod("SetColor"); + return new[] { + new CodeInstruction(OpCodes.Ldarg_0), + new CodeInstruction(OpCodes.Call, setColorM), + new CodeInstruction(OpCodes.Ret) + }; + } + + public static IEnumerable PatchResetColor(IEnumerable _) + { + var resetColor = typeof(ConsoleHarmonyPatches).GetMethod("ResetColor"); + return new[] { + new CodeInstruction(OpCodes.Call, resetColor), + new CodeInstruction(OpCodes.Ret) + }; + } + } + + private static HarmonyInstance harmony; + private static bool usingInterceptor = false; + + public static void Intercept() + { + if (!usingInterceptor) + { + usingInterceptor = true; + if (harmony == null) + harmony = HarmonyInstance.Create("BSIPA Console Redirector Patcher"); + if (stdoutInterceptor == null) + stdoutInterceptor = new StdoutInterceptor(); + if (stderrInterceptor == null) + stderrInterceptor = new StdoutInterceptor() { isStdErr = true }; + + RedirectConsole(); + ConsoleHarmonyPatches.Patch(harmony); + } + } + + public static void RedirectConsole() + { + if (usingInterceptor) + { + Console.SetOut(stdoutInterceptor); + Console.SetError(stderrInterceptor); + } + } + } +} diff --git a/IPA/obj/Debug/IPA.csproj.CoreCompileInputs.cache b/IPA/obj/Debug/IPA.csproj.CoreCompileInputs.cache index d16fba0d..5550c3c6 100644 --- a/IPA/obj/Debug/IPA.csproj.CoreCompileInputs.cache +++ b/IPA/obj/Debug/IPA.csproj.CoreCompileInputs.cache @@ -1 +1 @@ -5d76d76cc5c14257f2b9071c928c27b3edd80cc0 +b1078c6b90a14bd4bd5a5e7f04507fbb89e2b3a7 diff --git a/README.md b/README.md index 55150a1f..3ee33bec 100644 --- a/README.md +++ b/README.md @@ -47,9 +47,9 @@ BSIPA will automatically repatch the game when it updates, as long as `winhttp.d ### Prerequisites -- Microsoft Visual Studio 2017 or later -- Tools for C/C++ (MSVC) -- .NET 4.6 SDK and .NET 4.7.1 SDK +- Microsoft Visual Studio 2019 or later +- Tools for C/C++ (MSVC) v142 +- .NET 4.6.1 SDK and .NET 4.7.2 SDK ### Building