diff options
-rw-r--r-- | docs/release-notes.md | 4 | ||||
-rw-r--r-- | src/SMAPI/Framework/Logging/ConsoleInterceptionManager.cs | 59 | ||||
-rw-r--r-- | src/SMAPI/Framework/Logging/InterceptingTextWriter.cs | 55 | ||||
-rw-r--r-- | src/SMAPI/Framework/Logging/LogManager.cs | 15 | ||||
-rw-r--r-- | src/SMAPI/Framework/Monitor.cs | 19 |
5 files changed, 56 insertions, 96 deletions
diff --git a/docs/release-notes.md b/docs/release-notes.md index 2282bc3d..a65db68c 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,11 +9,12 @@ ## Upcoming release * For players: - * Removed the experimental `RewriteInParallel` option added in SMAPI 3.6 (it was already disabled by default). Unfortunately this caused intermittent unpredictable errors when enabled. + * Removed the experimental `RewriteInParallel` option added in SMAPI 3.6 (it was already disabled by default). Unfortunately this caused intermittent and unpredictable errors when enabled. * Tweaked the rules for showing update alerts (see _for SMAPI developers_ below for details). * Fixed crossplatform compatibility for mods which use the `[HarmonyPatch(type)]` attribute (thanks to spacechase0!). * Fixed broken URL in update alerts for unofficial versions. * Fixed rare error when a mod adds/removes event handlers asynchronously. + * Fixed rare issue where the console showed incorrect colors when mods wrote to it asynchronously. * For modders: * You can now read/write `SDate` values to JSON (e.g. for `config.json`, network mod messages, etc). @@ -23,6 +24,7 @@ * For SMAPI developers: * The web API now returns an update alert in two new cases: any newer unofficial update (previously only shown if the mod was incompatible), and a newer prerelease version if the installed non-prerelease version is broken (previously only shown if the installed version was prerelease). + * Internal refactoring to simplify future game updates. ## 3.6.2 Released 02 August 2020 for Stardew Valley 1.4.1 or later. diff --git a/src/SMAPI/Framework/Logging/ConsoleInterceptionManager.cs b/src/SMAPI/Framework/Logging/ConsoleInterceptionManager.cs deleted file mode 100644 index ef42e536..00000000 --- a/src/SMAPI/Framework/Logging/ConsoleInterceptionManager.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; - -namespace StardewModdingAPI.Framework.Logging -{ - /// <summary>Manages console output interception.</summary> - internal class ConsoleInterceptionManager : IDisposable - { - /********* - ** Fields - *********/ - /// <summary>The intercepting console writer.</summary> - private readonly InterceptingTextWriter Output; - - - /********* - ** Accessors - *********/ - /// <summary>The event raised when a message is written to the console directly.</summary> - public event Action<string> OnMessageIntercepted; - - - /********* - ** Public methods - *********/ - /// <summary>Construct an instance.</summary> - public ConsoleInterceptionManager() - { - // redirect output through interceptor - this.Output = new InterceptingTextWriter(Console.Out); - this.Output.OnMessageIntercepted += line => this.OnMessageIntercepted?.Invoke(line); - Console.SetOut(this.Output); - } - - /// <summary>Get an exclusive lock and write to the console output without interception.</summary> - /// <param name="action">The action to perform within the exclusive write block.</param> - public void ExclusiveWriteWithoutInterception(Action action) - { - lock (Console.Out) - { - try - { - this.Output.ShouldIntercept = false; - action(); - } - finally - { - this.Output.ShouldIntercept = true; - } - } - } - - /// <summary>Release all resources.</summary> - public void Dispose() - { - Console.SetOut(this.Output.Out); - this.Output.Dispose(); - } - } -} diff --git a/src/SMAPI/Framework/Logging/InterceptingTextWriter.cs b/src/SMAPI/Framework/Logging/InterceptingTextWriter.cs index 9ca61b59..d99f1dd2 100644 --- a/src/SMAPI/Framework/Logging/InterceptingTextWriter.cs +++ b/src/SMAPI/Framework/Logging/InterceptingTextWriter.cs @@ -8,17 +8,21 @@ namespace StardewModdingAPI.Framework.Logging internal class InterceptingTextWriter : TextWriter { /********* + ** Fields + *********/ + /// <summary>Prefixing a message with this character indicates that the console interceptor should write the string without intercepting it. (The character itself is not written.)</summary> + private readonly char IgnoreChar; + + + /********* ** Accessors *********/ /// <summary>The underlying console output.</summary> public TextWriter Out { get; } - /// <summary>The character encoding in which the output is written.</summary> + /// <inheritdoc /> public override Encoding Encoding => this.Out.Encoding; - /// <summary>Whether to intercept console output.</summary> - public bool ShouldIntercept { get; set; } - /// <summary>The event raised when a message is written to the console directly.</summary> public event Action<string> OnMessageIntercepted; @@ -28,36 +32,53 @@ namespace StardewModdingAPI.Framework.Logging *********/ /// <summary>Construct an instance.</summary> /// <param name="output">The underlying output writer.</param> - public InterceptingTextWriter(TextWriter output) + /// <param name="ignoreChar">Prefixing a message with this character indicates that the console interceptor should write the string without intercepting it. (The character itself is not written.)</param> + public InterceptingTextWriter(TextWriter output, char ignoreChar) { this.Out = output; + this.IgnoreChar = ignoreChar; } - /// <summary>Writes a subarray of characters to the text string or stream.</summary> - /// <param name="buffer">The character array to write data from.</param> - /// <param name="index">The character position in the buffer at which to start retrieving data.</param> - /// <param name="count">The number of characters to write.</param> + /// <inheritdoc /> public override void Write(char[] buffer, int index, int count) { - if (this.ShouldIntercept) - this.OnMessageIntercepted?.Invoke(new string(buffer, index, count).TrimEnd('\r', '\n')); - else + if (buffer.Length == 0) this.Out.Write(buffer, index, count); + else if (buffer[0] == this.IgnoreChar) + this.Out.Write(buffer, index + 1, count - 1); + else if (this.IsEmptyOrNewline(buffer)) + this.Out.Write(buffer, index, count); + else + this.OnMessageIntercepted?.Invoke(new string(buffer, index, count).TrimEnd('\r', '\n')); } - /// <summary>Writes a character to the text string or stream.</summary> - /// <param name="ch">The character to write to the text stream.</param> - /// <remarks>Console log messages from the game should be caught by <see cref="Write(char[],int,int)"/>. This method passes through anything that bypasses that method for some reason, since it's better to show it to users than hide it from everyone.</remarks> + /// <inheritdoc /> public override void Write(char ch) { this.Out.Write(ch); } - /// <summary>Releases the unmanaged resources used by the <see cref="T:System.IO.TextWriter" /> and optionally releases the managed resources.</summary> - /// <param name="disposing">true to release both managed and unmanaged resources; false to release only unmanaged resources.</param> + /// <inheritdoc /> protected override void Dispose(bool disposing) { this.OnMessageIntercepted = null; } + + + /********* + ** Private methods + *********/ + /// <summary>Get whether a buffer represents a line break.</summary> + /// <param name="buffer">The buffer to check.</param> + private bool IsEmptyOrNewline(char[] buffer) + { + foreach (char ch in buffer) + { + if (ch != '\n' && ch != '\r') + return false; + } + + return true; + } } } diff --git a/src/SMAPI/Framework/Logging/LogManager.cs b/src/SMAPI/Framework/Logging/LogManager.cs index 3786e940..d0936f3f 100644 --- a/src/SMAPI/Framework/Logging/LogManager.cs +++ b/src/SMAPI/Framework/Logging/LogManager.cs @@ -21,8 +21,8 @@ namespace StardewModdingAPI.Framework.Logging /// <summary>The log file to which to write messages.</summary> private readonly LogFileManager LogFile; - /// <summary>Manages console output interception.</summary> - private readonly ConsoleInterceptionManager InterceptionManager = new ConsoleInterceptionManager(); + /// <summary>Prefixing a low-level message with this character indicates that the console interceptor should write the string without intercepting it. (The character itself is not written.)</summary> + private readonly char IgnoreChar = '\u200B'; /// <summary>Get a named monitor instance.</summary> private readonly Func<string, Monitor> GetMonitorImpl; @@ -86,7 +86,7 @@ namespace StardewModdingAPI.Framework.Logging public LogManager(string logPath, ColorSchemeConfig colorConfig, bool writeToConsole, bool isVerbose, bool isDeveloperMode) { // init construction logic - this.GetMonitorImpl = name => new Monitor(name, this.InterceptionManager, this.LogFile, colorConfig, isVerbose) + this.GetMonitorImpl = name => new Monitor(name, this.IgnoreChar, this.LogFile, colorConfig, isVerbose) { WriteToConsole = writeToConsole, ShowTraceInConsole = isDeveloperMode, @@ -99,8 +99,10 @@ namespace StardewModdingAPI.Framework.Logging this.MonitorForGame = this.GetMonitor("game"); // redirect direct console output - if (this.MonitorForGame.WriteToConsole) - this.InterceptionManager.OnMessageIntercepted += message => this.HandleConsoleMessage(this.MonitorForGame, message); + var output = new InterceptingTextWriter(Console.Out, this.IgnoreChar); + if (writeToConsole) + output.OnMessageIntercepted += message => this.HandleConsoleMessage(this.MonitorForGame, message); + Console.SetOut(output); } /// <summary>Get a monitor instance derived from SMAPI's current settings.</summary> @@ -167,7 +169,7 @@ namespace StardewModdingAPI.Framework.Logging public void PressAnyKeyToExit(bool showMessage) { if (showMessage) - Console.WriteLine("Game has ended. Press any key to exit."); + this.Monitor.Log("Game has ended. Press any key to exit."); Thread.Sleep(100); Console.ReadKey(); Environment.Exit(0); @@ -339,7 +341,6 @@ namespace StardewModdingAPI.Framework.Logging /// <inheritdoc /> public void Dispose() { - this.InterceptionManager.Dispose(); this.LogFile.Dispose(); } diff --git a/src/SMAPI/Framework/Monitor.cs b/src/SMAPI/Framework/Monitor.cs index 44eeabe6..527cba64 100644 --- a/src/SMAPI/Framework/Monitor.cs +++ b/src/SMAPI/Framework/Monitor.cs @@ -18,8 +18,8 @@ namespace StardewModdingAPI.Framework /// <summary>Handles writing text to the console.</summary> private readonly IConsoleWriter ConsoleWriter; - /// <summary>Manages access to the console output.</summary> - private readonly ConsoleInterceptionManager ConsoleInterceptor; + /// <summary>Prefixing a message with this character indicates that the console interceptor should write the string without intercepting it. (The character itself is not written.)</summary> + private readonly char IgnoreChar; /// <summary>The log file to which to write messages.</summary> private readonly LogFileManager LogFile; @@ -52,11 +52,11 @@ namespace StardewModdingAPI.Framework *********/ /// <summary>Construct an instance.</summary> /// <param name="source">The name of the module which logs messages using this instance.</param> - /// <param name="consoleInterceptor">Intercepts access to the console output.</param> + /// <param name="ignoreChar">A character which indicates the message should not be intercepted if it appears as the first character of a string written to the console. The character itself is not logged in that case.</param> /// <param name="logFile">The log file to which to write messages.</param> /// <param name="colorConfig">The colors to use for text written to the SMAPI console.</param> /// <param name="isVerbose">Whether verbose logging is enabled. This enables more detailed diagnostic messages than are normally needed.</param> - public Monitor(string source, ConsoleInterceptionManager consoleInterceptor, LogFileManager logFile, ColorSchemeConfig colorConfig, bool isVerbose) + public Monitor(string source, char ignoreChar, LogFileManager logFile, ColorSchemeConfig colorConfig, bool isVerbose) { // validate if (string.IsNullOrWhiteSpace(source)) @@ -66,7 +66,7 @@ namespace StardewModdingAPI.Framework this.Source = source; this.LogFile = logFile ?? throw new ArgumentNullException(nameof(logFile), "The log file manager cannot be null."); this.ConsoleWriter = new ColorfulConsoleWriter(Constants.Platform, colorConfig); - this.ConsoleInterceptor = consoleInterceptor; + this.IgnoreChar = ignoreChar; this.IsVerbose = isVerbose; } @@ -99,7 +99,7 @@ namespace StardewModdingAPI.Framework internal void Newline() { if (this.WriteToConsole) - this.ConsoleInterceptor.ExclusiveWriteWithoutInterception(Console.WriteLine); + Console.WriteLine(); this.LogFile.WriteLine(""); } @@ -136,12 +136,7 @@ namespace StardewModdingAPI.Framework // write to console if (this.WriteToConsole && (this.ShowTraceInConsole || level != ConsoleLogLevel.Trace)) - { - this.ConsoleInterceptor.ExclusiveWriteWithoutInterception(() => - { - this.ConsoleWriter.WriteLine(consoleMessage, level); - }); - } + this.ConsoleWriter.WriteLine(this.IgnoreChar + consoleMessage, level); // write to log file this.LogFile.WriteLine(fullMessage); |