using System; using System.Collections.Generic; using System.Linq; using System.Threading; using StardewModdingAPI.Common; using StardewModdingAPI.Framework.Logging; using StardewModdingAPI.Framework.Models; namespace StardewModdingAPI.Framework { /// Encapsulates monitoring and logic for a given module. internal class Monitor : IMonitor { /********* ** Properties *********/ /// The name of the module which logs messages using this instance. private readonly string Source; /// Manages access to the console output. private readonly ConsoleInterceptionManager ConsoleManager; /// The log file to which to write messages. private readonly LogFileManager LogFile; /// The maximum length of the values. private static readonly int MaxLevelLength = (from level in Enum.GetValues(typeof(LogLevel)).Cast() select level.ToString().Length).Max(); /// The console text color for each log level. private readonly IDictionary Colors; /// Propagates notification that SMAPI should exit. private readonly CancellationTokenSource ExitTokenSource; /********* ** Accessors *********/ /// Whether SMAPI is aborting. Mods don't need to worry about this unless they have background tasks. public bool IsExiting => this.ExitTokenSource.IsCancellationRequested; /// Whether to show the full log stamps (with time/level/logger) in the console. If false, shows a simplified stamp with only the logger. internal bool ShowFullStampInConsole { get; set; } /// Whether to show trace messages in the console. internal bool ShowTraceInConsole { get; set; } /// Whether to write anything to the console. This should be disabled if no console is available. internal bool WriteToConsole { get; set; } = true; /********* ** Public methods *********/ /// Construct an instance. /// The name of the module which logs messages using this instance. /// Manages access to the console output. /// The log file to which to write messages. /// Propagates notification that SMAPI should exit. /// The console color scheme to use. public Monitor(string source, ConsoleInterceptionManager consoleManager, LogFileManager logFile, CancellationTokenSource exitTokenSource, MonitorColorScheme colorScheme) { // validate if (string.IsNullOrWhiteSpace(source)) throw new ArgumentException("The log source cannot be empty."); // initialise this.Colors = Monitor.GetConsoleColorScheme(colorScheme); this.Source = source; this.LogFile = logFile ?? throw new ArgumentNullException(nameof(logFile), "The log file manager cannot be null."); this.ConsoleManager = consoleManager; this.ExitTokenSource = exitTokenSource; } /// Log a message for the player or developer. /// The message to log. /// The log severity level. public void Log(string message, LogLevel level = LogLevel.Debug) { this.LogImpl(this.Source, message, level, this.Colors[level]); } /// Immediately exit the game without saving. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs. /// The reason for the shutdown. public void ExitGameImmediately(string reason) { this.LogFatal($"{this.Source} requested an immediate game shutdown: {reason}"); this.ExitTokenSource.Cancel(); } /// Write a newline to the console and log file. internal void Newline() { if (this.WriteToConsole) this.ConsoleManager.ExclusiveWriteWithoutInterception(Console.WriteLine); this.LogFile.WriteLine(""); } /// Log console input from the user. /// The user input to log. internal void LogUserInput(string input) { // user input already appears in the console, so just need to write to file string prefix = this.GenerateMessagePrefix(this.Source, LogLevel.Info); this.LogFile.WriteLine($"{prefix} $>{input}"); } /********* ** Private methods *********/ /// Log a fatal error message. /// The message to log. private void LogFatal(string message) { this.LogImpl(this.Source, message, LogLevel.Error, ConsoleColor.White, background: ConsoleColor.Red); } /// Write a message line to the log. /// The name of the mod logging the message. /// The message to log. /// The log level. /// The console foreground color. /// The console background color (or null to leave it as-is). private void LogImpl(string source, string message, LogLevel level, ConsoleColor color, ConsoleColor? background = null) { // generate message string prefix = this.GenerateMessagePrefix(source, level); string fullMessage = $"{prefix} {message}"; string consoleMessage = this.ShowFullStampInConsole ? fullMessage : $"[{source}] {message}"; // write to console if (this.WriteToConsole && (this.ShowTraceInConsole || level != LogLevel.Trace)) { this.ConsoleManager.ExclusiveWriteWithoutInterception(() => { if (this.ConsoleManager.SupportsColor) { if (background.HasValue) Console.BackgroundColor = background.Value; Console.ForegroundColor = color; Console.WriteLine(consoleMessage); Console.ResetColor(); } else Console.WriteLine(consoleMessage); }); } // write to log file this.LogFile.WriteLine(fullMessage); } /// Generate a message prefix for the current time. /// The name of the mod logging the message. /// The log level. private string GenerateMessagePrefix(string source, LogLevel level) { string levelStr = level.ToString().ToUpper().PadRight(Monitor.MaxLevelLength); return $"[{DateTime.Now:HH:mm:ss} {levelStr} {source}]"; } /// Get the color scheme to use for the current console. /// The console color scheme to use. private static IDictionary GetConsoleColorScheme(MonitorColorScheme colorScheme) { // auto detect color scheme if (colorScheme == MonitorColorScheme.AutoDetect) { if (Constants.TargetPlatform == Platform.Mac) colorScheme = MonitorColorScheme.LightBackground; // MacOS doesn't provide console background color info, but it's usually white. else colorScheme = Monitor.IsDark(Console.BackgroundColor) ? MonitorColorScheme.DarkBackground : MonitorColorScheme.LightBackground; } // get colors for scheme switch (colorScheme) { case MonitorColorScheme.DarkBackground: return new Dictionary { [LogLevel.Trace] = ConsoleColor.DarkGray, [LogLevel.Debug] = ConsoleColor.DarkGray, [LogLevel.Info] = ConsoleColor.White, [LogLevel.Warn] = ConsoleColor.Yellow, [LogLevel.Error] = ConsoleColor.Red, [LogLevel.Alert] = ConsoleColor.Magenta }; case MonitorColorScheme.LightBackground: return new Dictionary { [LogLevel.Trace] = ConsoleColor.DarkGray, [LogLevel.Debug] = ConsoleColor.DarkGray, [LogLevel.Info] = ConsoleColor.Black, [LogLevel.Warn] = ConsoleColor.DarkYellow, [LogLevel.Error] = ConsoleColor.Red, [LogLevel.Alert] = ConsoleColor.DarkMagenta }; default: throw new NotSupportedException($"Unknown color scheme '{colorScheme}'."); } } /// Get whether a console color should be considered dark, which is subjectively defined as 'white looks better than black on this text'. /// The color to check. private static bool IsDark(ConsoleColor color) { switch (color) { case ConsoleColor.Black: case ConsoleColor.Blue: case ConsoleColor.DarkBlue: case ConsoleColor.DarkMagenta: // Powershell case ConsoleColor.DarkRed: case ConsoleColor.Red: return true; default: return false; } } } }