diff options
author | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2021-01-08 21:18:15 -0500 |
---|---|---|
committer | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2021-01-08 21:18:15 -0500 |
commit | bdb7b04b3edecdf7dfa665d8b5a50a1bbff62699 (patch) | |
tree | 7249d86910ce37a93a82db8322fade5465445dc4 | |
parent | 04c6733adae9ce568aefb5d9dee6101097e994c5 (diff) | |
parent | df25368300e1254bc6eb2a1c7aa371bfeb289dff (diff) | |
download | SMAPI-bdb7b04b3edecdf7dfa665d8b5a50a1bbff62699.tar.gz SMAPI-bdb7b04b3edecdf7dfa665d8b5a50a1bbff62699.tar.bz2 SMAPI-bdb7b04b3edecdf7dfa665d8b5a50a1bbff62699.zip |
Merge branch 'develop' into stable
27 files changed, 268 insertions, 1484 deletions
diff --git a/build/common.targets b/build/common.targets index 4332313b..c7af8630 100644 --- a/build/common.targets +++ b/build/common.targets @@ -4,7 +4,7 @@ <!--set properties --> <PropertyGroup> - <Version>3.8.2</Version> + <Version>3.8.3</Version> <Product>SMAPI</Product> <LangVersion>latest</LangVersion> diff --git a/docs/release-notes.md b/docs/release-notes.md index 82709d3c..6a6b07a2 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -7,6 +7,21 @@ * Migrated to Harmony 2.0 (see [_migrate to Harmony 2.0_](https://stardewvalleywiki.com/Modding:Migrate_to_Harmony_2.0) for more info). --> +## 3.8.3 +Released 08 January 2021 for Stardew Valley 1.5.2 or later. + +* For players: + * Updated for Stardew Valley 1.5.2. + * Reduced memory usage. + * You can now enter console commands for a specific screen in split-screen mode by adding `screen=ID` to the command. + * Typing `help` in the SMAPI console is now more helpful. + +* For modders: + * Simplified tilesheet order warning added in SMAPI 3.8.2. + +* For the Console Commands mod: + * Removed experimental `performance` command. Unfortunately this impacted SMAPI's memory usage and performance, and the data was often misinterpreted. This may be replaced with more automatic performance alerts in a future version. + ## 3.8.2 Released 03 January 2021 for Stardew Valley 1.5.1 or later. diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs index e84445d7..7e157c38 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs @@ -1,7 +1,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Globalization; using System.Linq; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands @@ -107,38 +106,6 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands return true; } - /// <summary>Try to read a decimal argument.</summary> - /// <param name="index">The argument index.</param> - /// <param name="name">The argument name for error messages.</param> - /// <param name="value">The parsed value.</param> - /// <param name="required">Whether to show an error if the argument is missing.</param> - /// <param name="min">The minimum value allowed.</param> - /// <param name="max">The maximum value allowed.</param> - public bool TryGetDecimal(int index, string name, out decimal value, bool required = true, decimal? min = null, decimal? max = null) - { - value = 0; - - // get argument - if (!this.TryGet(index, name, out string raw, required)) - return false; - - // parse - if (!decimal.TryParse(raw, NumberStyles.Number, CultureInfo.InvariantCulture, out value)) - { - this.LogDecimalFormatError(index, name, min, max); - return false; - } - - // validate - if ((min.HasValue && value < min) || (max.HasValue && value > max)) - { - this.LogDecimalFormatError(index, name, min, max); - return false; - } - - return true; - } - /// <summary>Returns an enumerator that iterates through the collection.</summary> /// <returns>An enumerator that can be used to iterate through the collection.</returns> public IEnumerator<string> GetEnumerator() @@ -180,22 +147,5 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands else this.LogError($"Argument {index} ({name}) must be an integer."); } - - /// <summary>Print an error for an invalid decimal argument.</summary> - /// <param name="index">The argument index.</param> - /// <param name="name">The argument name for error messages.</param> - /// <param name="min">The minimum value allowed.</param> - /// <param name="max">The maximum value allowed.</param> - private void LogDecimalFormatError(int index, string name, decimal? min, decimal? max) - { - if (min.HasValue && max.HasValue) - this.LogError($"Argument {index} ({name}) must be a decimal between {min} and {max}."); - else if (min.HasValue) - this.LogError($"Argument {index} ({name}) must be a decimal and at least {min}."); - else if (max.HasValue) - this.LogError($"Argument {index} ({name}) must be a decimal and at most {max}."); - else - this.LogError($"Argument {index} ({name}) must be a decimal."); - } } } diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs deleted file mode 100644 index 63851c9d..00000000 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs +++ /dev/null @@ -1,647 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using StardewModdingAPI.Framework; -using StardewModdingAPI.Framework.PerformanceMonitoring; - -namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other -{ - /// <summary>A set of commands which displays or configures performance monitoring.</summary> - internal class PerformanceCounterCommand : TrainerCommand - { - /********* - ** Fields - *********/ - /// <summary>The name of the command.</summary> - private const string CommandName = "performance"; - - /// <summary>The available commands.</summary> - private enum SubCommand - { - Summary, - Detail, - Reset, - Trigger, - Enable, - Disable, - Help - } - - - /********* - ** Public methods - *********/ - /// <summary>Construct an instance.</summary> - public PerformanceCounterCommand() - : base(CommandName, PerformanceCounterCommand.GetDescription()) { } - - /// <summary>Handle the command.</summary> - /// <param name="monitor">Writes messages to the console and log file.</param> - /// <param name="command">The command name.</param> - /// <param name="args">The command arguments.</param> - public override void Handle(IMonitor monitor, string command, ArgumentParser args) - { - // parse args - SubCommand subcommand = SubCommand.Summary; - { - if (args.TryGet(0, "command", out string subcommandStr, false) && !Enum.TryParse(subcommandStr, ignoreCase: true, out subcommand)) - { - this.LogUsageError(monitor, $"Unknown command {subcommandStr}"); - return; - } - } - - // handle - switch (subcommand) - { - case SubCommand.Summary: - this.HandleSummarySubCommand(monitor, args); - break; - - case SubCommand.Detail: - this.HandleDetailSubCommand(monitor, args); - break; - - case SubCommand.Reset: - this.HandleResetSubCommand(monitor, args); - break; - - case SubCommand.Trigger: - this.HandleTriggerSubCommand(monitor, args); - break; - - case SubCommand.Enable: - SCore.PerformanceMonitor.EnableTracking = true; - monitor.Log("Performance counter tracking is now enabled", LogLevel.Info); - break; - - case SubCommand.Disable: - SCore.PerformanceMonitor.EnableTracking = false; - monitor.Log("Performance counter tracking is now disabled", LogLevel.Info); - break; - - case SubCommand.Help: - this.OutputHelp(monitor, args.TryGet(1, "command", out _) ? subcommand : null as SubCommand?); - break; - - default: - this.LogUsageError(monitor, $"Unknown command {subcommand}"); - break; - } - } - - - /********* - ** Private methods - *********/ - /// <summary>Handles the summary sub command.</summary> - /// <param name="monitor">Writes messages to the console and log file.</param> - /// <param name="args">The command arguments.</param> - private void HandleSummarySubCommand(IMonitor monitor, ArgumentParser args) - { - if (!this.AssertEnabled(monitor)) - return; - - IEnumerable<PerformanceCounterCollection> data = SCore.PerformanceMonitor.GetCollections(); - - double? threshold = null; - if (args.TryGetDecimal(1, "threshold", out decimal t, required: false)) - threshold = (double?)t; - - TimeSpan interval = TimeSpan.FromSeconds(60); - - StringBuilder report = new StringBuilder(); - report.AppendLine($"Summary over the last {interval.TotalSeconds} seconds:"); - report.AppendLine(this.GetTableString( - data: data, - header: new[] { "Collection", "Avg Calls/s", "Avg Exec Time (Game)", "Avg Exec Time (Mods)", "Avg Exec Time (Game+Mods)", "Peak Exec Time" }, - getRow: item => new[] - { - item.Name, - item.GetAverageCallsPerSecond().ToString(), - this.FormatMilliseconds(item.GetGameAverageExecutionTime(interval), threshold), - this.FormatMilliseconds(item.GetModsAverageExecutionTime(interval), threshold), - this.FormatMilliseconds(item.GetAverageExecutionTime(interval), threshold), - this.FormatMilliseconds(item.GetPeakExecutionTime(interval), threshold) - }, - true - )); - - monitor.Log(report.ToString(), LogLevel.Info); - } - - /// <summary>Handles the detail sub command.</summary> - /// <param name="monitor">Writes messages to the console and log file.</param> - /// <param name="args">The command arguments.</param> - private void HandleDetailSubCommand(IMonitor monitor, ArgumentParser args) - { - if (!this.AssertEnabled(monitor)) - return; - - // parse args - double thresholdMilliseconds = 0; - if (args.TryGetDecimal(1, "threshold", out decimal t, required: false)) - thresholdMilliseconds = (double)t; - - // get collections - var collections = SCore.PerformanceMonitor.GetCollections(); - - // render - TimeSpan averageInterval = TimeSpan.FromSeconds(60); - StringBuilder report = new StringBuilder($"Showing details for performance counters of {thresholdMilliseconds}+ milliseconds:\n\n"); - bool anyShown = false; - foreach (PerformanceCounterCollection collection in collections) - { - KeyValuePair<string, PerformanceCounter>[] data = collection.PerformanceCounters - .Where(p => p.Value.GetAverage(averageInterval) >= thresholdMilliseconds) - .ToArray(); - - if (data.Any()) - { - anyShown = true; - report.AppendLine($"{collection.Name}:"); - report.AppendLine(this.GetTableString( - data: data, - header: new[] { "Mod", $"Avg Exec Time (last {(int)averageInterval.TotalSeconds}s)", "Last Exec Time", "Peak Exec Time", $"Peak Exec Time (last {(int)averageInterval.TotalSeconds}s)" }, - getRow: item => new[] - { - item.Key, - this.FormatMilliseconds(item.Value.GetAverage(averageInterval), thresholdMilliseconds), - this.FormatMilliseconds(item.Value.GetLastEntry()?.ElapsedMilliseconds), - this.FormatMilliseconds(item.Value.GetPeak()?.ElapsedMilliseconds), - this.FormatMilliseconds(item.Value.GetPeak(averageInterval)?.ElapsedMilliseconds) - }, - true - )); - } - } - - if (!anyShown) - report.AppendLine("No performance counters found."); - - monitor.Log(report.ToString(), LogLevel.Info); - } - - /// <summary>Handles the trigger sub command.</summary> - /// <param name="monitor">Writes messages to the console and log file.</param> - /// <param name="args">The command arguments.</param> - private void HandleTriggerSubCommand(IMonitor monitor, ArgumentParser args) - { - if (!this.AssertEnabled(monitor)) - return; - - if (args.TryGet(1, "mode", out string mode, false)) - { - switch (mode) - { - case "list": - this.OutputAlertTriggers(monitor); - break; - - case "collection": - if (args.TryGet(2, "name", out string collectionName)) - { - if (args.TryGetDecimal(3, "threshold", out decimal threshold)) - { - if (!args.TryGet(4, "source", out string source, required: false)) - source = null; - this.ConfigureAlertTrigger(monitor, collectionName, source, threshold); - } - } - break; - - case "pause": - SCore.PerformanceMonitor.PauseAlerts = true; - monitor.Log("Alerts are now paused.", LogLevel.Info); - break; - - case "resume": - SCore.PerformanceMonitor.PauseAlerts = false; - monitor.Log("Alerts are now resumed.", LogLevel.Info); - break; - - case "dump": - this.OutputAlertTriggers(monitor, true); - break; - - case "clear": - this.ClearAlertTriggers(monitor); - break; - - default: - this.LogUsageError(monitor, $"Unknown mode {mode}. See '{CommandName} help trigger' for usage."); - break; - } - } - else - this.OutputAlertTriggers(monitor); - } - - /// <summary>Sets up an an alert trigger.</summary> - /// <param name="monitor">Writes messages to the console and log file.</param> - /// <param name="collectionName">The name of the collection.</param> - /// <param name="sourceName">The name of the source, or null for all sources.</param> - /// <param name="threshold">The trigger threshold, or 0 to remove.</param> - private void ConfigureAlertTrigger(IMonitor monitor, string collectionName, string sourceName, decimal threshold) - { - foreach (PerformanceCounterCollection collection in SCore.PerformanceMonitor.GetCollections()) - { - if (collection.Name.ToLowerInvariant().Equals(collectionName.ToLowerInvariant())) - { - if (sourceName == null) - { - if (threshold != 0) - { - collection.EnableAlerts = true; - collection.AlertThresholdMilliseconds = (double)threshold; - monitor.Log($"Set up alert triggering for '{collectionName}' with '{this.FormatMilliseconds((double?)threshold)}'", LogLevel.Info); - } - else - { - collection.EnableAlerts = false; - monitor.Log($"Cleared alert triggering for '{collection}'."); - } - - return; - } - else - { - foreach (var performanceCounter in collection.PerformanceCounters) - { - if (performanceCounter.Value.Source.ToLowerInvariant().Equals(sourceName.ToLowerInvariant())) - { - if (threshold != 0) - { - performanceCounter.Value.EnableAlerts = true; - performanceCounter.Value.AlertThresholdMilliseconds = (double)threshold; - monitor.Log($"Set up alert triggering for '{sourceName}' in collection '{collectionName}' with '{this.FormatMilliseconds((double?)threshold)}", LogLevel.Info); - } - else - performanceCounter.Value.EnableAlerts = false; - return; - } - } - - monitor.Log($"Could not find the source '{sourceName}' in collection '{collectionName}'", LogLevel.Warn); - return; - } - } - } - - monitor.Log($"Could not find the collection '{collectionName}'", LogLevel.Warn); - } - - - /// <summary>Clears alert triggering for all collections.</summary> - /// <param name="monitor">Writes messages to the console and log file.</param> - private void ClearAlertTriggers(IMonitor monitor) - { - int clearedTriggers = 0; - foreach (PerformanceCounterCollection collection in SCore.PerformanceMonitor.GetCollections()) - { - if (collection.EnableAlerts) - { - collection.EnableAlerts = false; - clearedTriggers++; - } - - foreach (var performanceCounter in collection.PerformanceCounters) - { - if (performanceCounter.Value.EnableAlerts) - { - performanceCounter.Value.EnableAlerts = false; - clearedTriggers++; - } - } - - } - - monitor.Log($"Cleared {clearedTriggers} alert triggers.", LogLevel.Info); - } - - /// <summary>Lists all configured alert triggers.</summary> - /// <param name="monitor">Writes messages to the console and log file.</param> - /// <param name="asDump">True to dump the triggers as commands.</param> - private void OutputAlertTriggers(IMonitor monitor, bool asDump = false) - { - StringBuilder report = new StringBuilder(); - report.AppendLine("Configured triggers:"); - report.AppendLine(); - var collectionTriggers = new List<CollectionTrigger>(); - var sourceTriggers = new List<SourceTrigger>(); - - foreach (PerformanceCounterCollection collection in SCore.PerformanceMonitor.GetCollections()) - { - if (collection.EnableAlerts) - collectionTriggers.Add(new CollectionTrigger(collection.Name, collection.AlertThresholdMilliseconds)); - - sourceTriggers.AddRange( - from counter in collection.PerformanceCounters - where counter.Value.EnableAlerts - select new SourceTrigger(collection.Name, counter.Value.Source, counter.Value.AlertThresholdMilliseconds) - ); - } - - if (collectionTriggers.Count > 0) - { - report.AppendLine("Collection Triggers:"); - report.AppendLine(); - - if (asDump) - { - foreach (var item in collectionTriggers) - report.AppendLine($"{CommandName} trigger {item.CollectionName} {item.Threshold}"); - } - else - { - report.AppendLine(this.GetTableString( - data: collectionTriggers, - header: new[] { "Collection", "Threshold" }, - getRow: item => new[] { item.CollectionName, this.FormatMilliseconds(item.Threshold) }, - true - )); - } - - report.AppendLine(); - } - else - report.AppendLine("No collection triggers."); - - if (sourceTriggers.Count > 0) - { - report.AppendLine("Source Triggers:"); - report.AppendLine(); - - if (asDump) - { - foreach (SourceTrigger item in sourceTriggers) - report.AppendLine($"{CommandName} trigger {item.CollectionName} {item.Threshold} {item.SourceName}"); - } - else - { - report.AppendLine(this.GetTableString( - data: sourceTriggers, - header: new[] { "Collection", "Source", "Threshold" }, - getRow: item => new[] { item.CollectionName, item.SourceName, this.FormatMilliseconds(item.Threshold) }, - true - )); - } - - report.AppendLine(); - } - else - report.AppendLine("No source triggers."); - - monitor.Log(report.ToString(), LogLevel.Info); - } - - /// <summary>Handles the reset sub command.</summary> - /// <param name="monitor">Writes messages to the console and log file.</param> - /// <param name="args">The command arguments.</param> - private void HandleResetSubCommand(IMonitor monitor, ArgumentParser args) - { - if (!this.AssertEnabled(monitor)) - return; - - if (args.TryGet(1, "type", out string type, false, new[] { "category", "source" })) - { - args.TryGet(2, "name", out string name); - - switch (type) - { - case "category": - SCore.PerformanceMonitor.ResetCollection(name); - monitor.Log($"All performance counters for category {name} are now cleared.", LogLevel.Info); - break; - case "source": - SCore.PerformanceMonitor.ResetSource(name); - monitor.Log($"All performance counters for source {name} are now cleared.", LogLevel.Info); - break; - } - } - else - { - SCore.PerformanceMonitor.Reset(); - monitor.Log("All performance counters are now cleared.", LogLevel.Info); - } - } - - /// <summary>Formats the given milliseconds value into a string format. Optionally - /// allows a threshold to return "-" if the value is less than the threshold.</summary> - /// <param name="milliseconds">The milliseconds to format. Returns "-" if null</param> - /// <param name="thresholdMilliseconds">The threshold. Any value below this is returned as "-".</param> - /// <returns>The formatted milliseconds.</returns> - private string FormatMilliseconds(double? milliseconds, double? thresholdMilliseconds = null) - { - thresholdMilliseconds ??= 1; - return milliseconds != null && milliseconds >= thresholdMilliseconds - ? ((double)milliseconds).ToString("F2") - : "-"; - } - - /// <summary>Shows detailed help for a specific sub command.</summary> - /// <param name="monitor">The output monitor.</param> - /// <param name="subcommand">The subcommand.</param> - private void OutputHelp(IMonitor monitor, SubCommand? subcommand) - { - StringBuilder report = new StringBuilder(); - report.AppendLine(); - - switch (subcommand) - { - case SubCommand.Detail: - report.AppendLine($" {CommandName} detail <threshold>"); - report.AppendLine(); - report.AppendLine("Displays details for a specific collection."); - report.AppendLine(); - report.AppendLine("Arguments:"); - report.AppendLine(" <threshold> Optional. The threshold in milliseconds. Any average execution time below that"); - report.AppendLine(" threshold is not reported."); - report.AppendLine(); - report.AppendLine("Examples:"); - report.AppendLine($"{CommandName} detail 5 Show counters exceeding an average of 5ms"); - break; - - case SubCommand.Summary: - report.AppendLine($"Usage: {CommandName} summary <threshold>"); - report.AppendLine(); - report.AppendLine("Displays the performance counter summary."); - report.AppendLine(); - report.AppendLine("Arguments:"); - report.AppendLine(" <threshold> Optional. Hides the actual execution time if it's below this threshold"); - report.AppendLine(); - report.AppendLine("Examples:"); - report.AppendLine($"{CommandName} summary Show all events"); - report.AppendLine($"{CommandName} summary 5 Shows events exceeding an average of 5ms"); - break; - - case SubCommand.Trigger: - report.AppendLine($"Usage: {CommandName} trigger <mode>"); - report.AppendLine($"Usage: {CommandName} trigger collection <collectionName> <threshold>"); - report.AppendLine($"Usage: {CommandName} trigger collection <collectionName> <threshold> <sourceName>"); - report.AppendLine(); - report.AppendLine("Manages alert triggers."); - report.AppendLine(); - report.AppendLine("Arguments:"); - report.AppendLine(" <mode> Optional. Specifies if a specific source or a specific collection should be triggered."); - report.AppendLine(" - list Lists current triggers"); - report.AppendLine(" - collection Sets up a trigger for a collection"); - report.AppendLine(" - clear Clears all trigger entries"); - report.AppendLine(" - pause Pauses triggering of alerts"); - report.AppendLine(" - resume Resumes triggering of alerts"); - report.AppendLine(" - dump Dumps all triggers as commands for copy and paste"); - report.AppendLine(" Defaults to 'list' if not specified."); - report.AppendLine(); - report.AppendLine(" <collectionName> Required if the mode 'collection' is specified."); - report.AppendLine(" Specifies the name of the collection to be triggered. Must be an exact match."); - report.AppendLine(); - report.AppendLine(" <sourceName> Optional. Specifies the name of a specific source. Must be an exact match."); - report.AppendLine(); - report.AppendLine(" <threshold> Required if the mode 'collection' is specified."); - report.AppendLine(" Specifies the threshold in milliseconds (fractions allowed)."); - report.AppendLine(" Specify '0' to remove the threshold."); - report.AppendLine(); - report.AppendLine("Examples:"); - report.AppendLine(); - report.AppendLine($"{CommandName} trigger collection Display.Rendering 10"); - report.AppendLine(" Sets up an alert trigger which writes on the console if the execution time of all performance counters in"); - report.AppendLine(" the 'Display.Rendering' collection exceed 10 milliseconds."); - report.AppendLine(); - report.AppendLine($"{CommandName} trigger collection Display.Rendering 5 Pathoschild.ChestsAnywhere"); - report.AppendLine(" Sets up an alert trigger to write on the console if the execution time of Pathoschild.ChestsAnywhere in"); - report.AppendLine(" the 'Display.Rendering' collection exceed 5 milliseconds."); - report.AppendLine(); - report.AppendLine($"{CommandName} trigger collection Display.Rendering 0"); - report.AppendLine(" Removes the threshold previously defined from the collection. Note that source-specific thresholds are left intact."); - report.AppendLine(); - report.AppendLine($"{CommandName} trigger clear"); - report.AppendLine(" Clears all previously setup alert triggers."); - break; - - case SubCommand.Reset: - report.AppendLine($"Usage: {CommandName} reset <type> <name>"); - report.AppendLine(); - report.AppendLine("Resets performance counters."); - report.AppendLine(); - report.AppendLine("Arguments:"); - report.AppendLine(" <type> Optional. Specifies if a collection or source should be reset."); - report.AppendLine(" If omitted, all performance counters are reset."); - report.AppendLine(); - report.AppendLine(" - source Clears performance counters for a specific source"); - report.AppendLine(" - collection Clears performance counters for a specific collection"); - report.AppendLine(); - report.AppendLine(" <name> Required if a <type> is given. Specifies the name of either the collection"); - report.AppendLine(" or the source. The name must be an exact match."); - report.AppendLine(); - report.AppendLine("Examples:"); - report.AppendLine($"{CommandName} reset Resets all performance counters"); - report.AppendLine($"{CommandName} reset source Pathoschild.ChestsAnywhere Resets all performance for the source named Pathoschild.ChestsAnywhere"); - report.AppendLine($"{CommandName} reset collection Display.Rendering Resets all performance for the collection named Display.Rendering"); - break; - } - - report.AppendLine(); - monitor.Log(report.ToString(), LogLevel.Info); - } - - /// <summary>Get the command description.</summary> - private static string GetDescription() - { - StringBuilder report = new StringBuilder(); - - report.AppendLine("Displays or configures performance monitoring to diagnose issues. Performance monitoring is disabled by default."); - report.AppendLine(); - report.AppendLine("For example, the counter collection named 'Display.Rendered' contains one performance"); - report.AppendLine("counter when the game executes the 'Display.Rendered' event, and another counter for each mod which handles it."); - report.AppendLine(); - report.AppendLine($"Usage: {CommandName} <command> <action>"); - report.AppendLine(); - report.AppendLine("Commands:"); - report.AppendLine(); - report.AppendLine(" summary Show a summary of collections."); - report.AppendLine(" detail Show a summary for a given collection."); - report.AppendLine(" reset Reset all performance counters."); - report.AppendLine(" trigger Configure alert triggers."); - report.AppendLine(" enable Enable performance counter recording."); - report.AppendLine(" disable Disable performance counter recording."); - report.AppendLine(" help Show verbose help for the available commands."); - report.AppendLine(); - report.AppendLine($"To get help for a specific command, use '{CommandName} help <command>', for example:"); - report.AppendLine($"{CommandName} help summary"); - report.AppendLine(); - report.AppendLine("Defaults to summary if no command is given."); - report.AppendLine(); - - return report.ToString(); - } - - /// <summary>Log a warning if performance monitoring isn't enabled.</summary> - /// <param name="monitor">Writes messages to the console and log file.</param> - /// <returns>Returns whether performance monitoring is enabled.</returns> - private bool AssertEnabled(IMonitor monitor) - { - if (!SCore.PerformanceMonitor.EnableTracking) - { - monitor.Log($"Performance monitoring is currently disabled; enter '{CommandName} enable' to enable it.", LogLevel.Warn); - return false; - } - - return true; - } - - - /********* - ** Private models - *********/ - /// <summary>An alert trigger for a collection.</summary> - private class CollectionTrigger - { - /********* - ** Accessors - *********/ - /// <summary>The collection name.</summary> - public string CollectionName { get; } - - /// <summary>The trigger threshold.</summary> - public double Threshold { get; } - - - /********* - ** Public methods - *********/ - /// <summary>Construct an instance.</summary> - /// <param name="collectionName">The collection name.</param> - /// <param name="threshold">The trigger threshold.</param> - public CollectionTrigger(string collectionName, double threshold) - { - this.CollectionName = collectionName; - this.Threshold = threshold; - } - } - - /// <summary>An alert triggered for a source.</summary> - private class SourceTrigger : CollectionTrigger - { - /********* - ** Accessors - *********/ - /// <summary>The source name.</summary> - public string SourceName { get; } - - - /********* - ** Public methods - *********/ - /// <summary>Construct an instance.</summary> - /// <param name="collectionName">The collection name.</param> - /// <param name="sourceName">The source name.</param> - /// <param name="threshold">The trigger threshold.</param> - public SourceTrigger(string collectionName, string sourceName, double threshold) - : base(collectionName, threshold) - { - this.SourceName = sourceName; - } - } - } -} diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs index 77a26c6a..98daa906 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs @@ -78,8 +78,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands /// <param name="data">The data to display.</param> /// <param name="header">The table header.</param> /// <param name="getRow">Returns a set of fields for a data value.</param> - /// <param name="rightAlign">Whether to right-align the data.</param> - protected string GetTableString<T>(IEnumerable<T> data, string[] header, Func<T, string[]> getRow, bool rightAlign = false) + protected string GetTableString<T>(IEnumerable<T> data, string[] header, Func<T, string[]> getRow) { // get table data int[] widths = header.Select(p => p.Length).ToArray(); @@ -108,7 +107,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands return string.Join( Environment.NewLine, lines.Select(line => string.Join(" | ", - line.Select((field, i) => rightAlign ? field.PadRight(widths[i], ' ') : field.PadLeft(widths[i], ' ')) + line.Select((field, i) => field.PadLeft(widths[i], ' ')) )) ); } diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index 13e85c70..1a021485 100644 --- a/src/SMAPI.Mods.ConsoleCommands/manifest.json +++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json @@ -1,9 +1,9 @@ { "Name": "Console Commands", "Author": "SMAPI", - "Version": "3.8.2", + "Version": "3.8.3", "Description": "Adds SMAPI console commands that let you manipulate the game.", "UniqueID": "SMAPI.ConsoleCommands", "EntryDll": "ConsoleCommands.dll", - "MinimumApiVersion": "3.8.2" + "MinimumApiVersion": "3.8.3" } diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json index 475ec165..17ca5991 100644 --- a/src/SMAPI.Mods.SaveBackup/manifest.json +++ b/src/SMAPI.Mods.SaveBackup/manifest.json @@ -1,9 +1,9 @@ { "Name": "Save Backup", "Author": "SMAPI", - "Version": "3.8.2", + "Version": "3.8.3", "Description": "Automatically backs up all your saves once per day into its folder.", "UniqueID": "SMAPI.SaveBackup", "EntryDll": "SaveBackup.dll", - "MinimumApiVersion": "3.8.2" + "MinimumApiVersion": "3.8.3" } diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index 586b0c3c..bd1f8c9b 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -210,6 +210,7 @@ namespace StardewModdingAPI.Web [@"^/community\.?$"] = "https://stardewvalleywiki.com/Modding:Community", [@"^/compat\.?$"] = "https://smapi.io/mods", [@"^/docs\.?$"] = "https://stardewvalleywiki.com/Modding:Index", + [@"^/help\.?$"] = "https://stardewvalleywiki.com/Modding:Help", [@"^/install\.?$"] = "https://stardewvalleywiki.com/Modding:Player_Guide/Getting_Started#Install_SMAPI", [@"^/troubleshoot(.*)$"] = "https://stardewvalleywiki.com/Modding:Player_Guide/Troubleshooting$1", [@"^/xnb\.?$"] = "https://stardewvalleywiki.com/Modding:Using_XNB_mods" diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index ef996c0f..2c0b85b7 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -54,10 +54,10 @@ namespace StardewModdingAPI ** Public ****/ /// <summary>SMAPI's current semantic version.</summary> - public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.8.2"); + public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.8.3"); /// <summary>The minimum supported version of Stardew Valley.</summary> - public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.5.1"); + public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.5.2"); /// <summary>The maximum supported version of Stardew Valley.</summary> public static ISemanticVersion MaximumGameVersion { get; } = null; @@ -97,9 +97,6 @@ namespace StardewModdingAPI /// <summary>The URL of the SMAPI home page.</summary> internal const string HomePageUrl = "https://smapi.io"; - /// <summary>The default performance counter name for unknown event handlers.</summary> - internal const string GamePerformanceCounterName = "<StardewValley>"; - /// <summary>The absolute path to the folder containing SMAPI's internal files.</summary> internal static readonly string InternalFilesPath = EarlyConstants.InternalFilesPath; diff --git a/src/SMAPI/Framework/CommandManager.cs b/src/SMAPI/Framework/CommandManager.cs index 4a99fd4d..ff540ad8 100644 --- a/src/SMAPI/Framework/CommandManager.cs +++ b/src/SMAPI/Framework/CommandManager.cs @@ -15,10 +15,20 @@ namespace StardewModdingAPI.Framework /// <summary>The commands registered with SMAPI.</summary> private readonly IDictionary<string, Command> Commands = new Dictionary<string, Command>(StringComparer.OrdinalIgnoreCase); + /// <summary>Writes messages to the console.</summary> + private readonly IMonitor Monitor; + /********* ** Public methods *********/ + /// <summary>Construct an instance.</summary> + /// <param name="monitor">Writes messages to the console.</param> + public CommandManager(IMonitor monitor) + { + this.Monitor = monitor; + } + /// <summary>Add a console command.</summary> /// <param name="mod">The mod adding the command (or <c>null</c> for a SMAPI command).</param> /// <param name="name">The command name, which the user must type to trigger it.</param> @@ -81,8 +91,9 @@ namespace StardewModdingAPI.Framework /// <param name="name">The parsed command name.</param> /// <param name="args">The parsed command arguments.</param> /// <param name="command">The command which can handle the input.</param> + /// <param name="screenId">The screen ID on which to run the command.</param> /// <returns>Returns true if the input was successfully parsed and matched to a command; else false.</returns> - public bool TryParse(string input, out string name, out string[] args, out Command command) + public bool TryParse(string input, out string name, out string[] args, out Command command, out int screenId) { // ignore if blank if (string.IsNullOrWhiteSpace(input)) @@ -90,6 +101,7 @@ namespace StardewModdingAPI.Framework name = null; args = null; command = null; + screenId = 0; return false; } @@ -98,6 +110,27 @@ namespace StardewModdingAPI.Framework name = this.GetNormalizedName(args[0]); args = args.Skip(1).ToArray(); + // get screen ID argument + screenId = 0; + for (int i = 0; i < args.Length; i++) + { + // consume arg & set screen ID + if (this.TryParseScreenId(args[i], out int rawScreenId, out string error)) + { + args = args.Take(i).Concat(args.Skip(i + 1)).ToArray(); + screenId = rawScreenId; + continue; + } + + // invalid screen arg + if (error != null) + { + this.Monitor.Log(error, LogLevel.Error); + command = null; + return false; + } + } + // get command return this.Commands.TryGetValue(name, out command); } @@ -152,6 +185,38 @@ namespace StardewModdingAPI.Framework return args.Where(item => !string.IsNullOrWhiteSpace(item)).ToArray(); } + /// <summary>Try to parse a 'screen=X' command argument, which specifies the screen that should receive the command.</summary> + /// <param name="arg">The raw argument to parse.</param> + /// <param name="screen">The parsed screen ID, if any.</param> + /// <param name="error">The error which indicates an invalid screen ID, if applicable.</param> + /// <returns>Returns whether the screen ID was parsed successfully.</returns> + private bool TryParseScreenId(string arg, out int screen, out string error) + { + screen = -1; + error = null; + + // skip non-screen arg + if (!arg.StartsWith("screen=")) + return false; + + // get screen ID + string rawScreen = arg.Substring("screen=".Length); + if (!int.TryParse(rawScreen, out screen)) + { + error = $"invalid screen ID format: {rawScreen}"; + return false; + } + + // validate ID + if (!Context.HasScreenId(screen)) + { + error = $"there's no active screen with ID {screen}. Active screen IDs: {string.Join(", ", Context.ActiveScreenIds)}."; + return false; + } + + return true; + } + /// <summary>Get a normalized command name.</summary> /// <param name="name">The command name.</param> private string GetNormalizedName(string name) diff --git a/src/SMAPI/Framework/Commands/HelpCommand.cs b/src/SMAPI/Framework/Commands/HelpCommand.cs index b8730a00..baf3116e 100644 --- a/src/SMAPI/Framework/Commands/HelpCommand.cs +++ b/src/SMAPI/Framework/Commands/HelpCommand.cs @@ -41,13 +41,26 @@ namespace StardewModdingAPI.Framework.Commands { Command result = this.CommandManager.Get(args[0]); if (result == null) - monitor.Log("There's no command with that name.", LogLevel.Error); + monitor.Log("There's no command with that name. Type 'help' by itself for more info.", LogLevel.Error); else monitor.Log($"{result.Name}: {result.Documentation}{(result.Mod != null ? $"\n(Added by {result.Mod.DisplayName}.)" : "")}", LogLevel.Info); } else { - string message = "The following commands are registered:\n"; + string message = + "\n\n" + + "Need help with a SMAPI or mod issue?\n" + + "------------------------------------\n" + + "See https://smapi.io/help for the best places to ask.\n\n\n" + + "How commands work\n" + + "-----------------\n" + + "Just enter a command directly to run it, just like you did for this help command. Commands may take optional arguments\n" + + "which change what they do; for example, type 'help help' to see help about the help command. When playing in split-screen\n" + + "mode, you can add screen=X to send the command to a specific screen instance.\n\n\n" + + "Valid commands\n" + + "--------------\n" + + "The following commands are registered. For more info about a command, type 'help command_name'.\n\n"; + IGrouping<string, string>[] groups = (from command in this.CommandManager.GetAll() orderby command.Mod?.DisplayName, command.Name group command.Name by command.Mod?.DisplayName).ToArray(); foreach (var group in groups) { @@ -55,7 +68,6 @@ namespace StardewModdingAPI.Framework.Commands string[] commandNames = group.ToArray(); message += $"{modName}:\n {string.Join("\n ", commandNames)}\n\n"; } - message += "For more information about a command, type 'help command_name'."; monitor.Log(message, LogLevel.Info); } diff --git a/src/SMAPI/Framework/Content/TilesheetReference.cs b/src/SMAPI/Framework/Content/TilesheetReference.cs new file mode 100644 index 00000000..2ea38430 --- /dev/null +++ b/src/SMAPI/Framework/Content/TilesheetReference.cs @@ -0,0 +1,33 @@ +namespace StardewModdingAPI.Framework.Content +{ + /// <summary>Basic metadata about a vanilla tilesheet.</summary> + internal class TilesheetReference + { + /********* + ** Accessors + *********/ + /// <summary>The tilesheet's index in the list.</summary> + public readonly int Index; + + /// <summary>The tilesheet's unique ID in the map.</summary> + public readonly string Id; + + /// <summary>The asset path for the tilesheet texture.</summary> + public readonly string ImageSource; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="index">The tilesheet's index in the list.</param> + /// <param name="id">The tilesheet's unique ID in the map.</param> + /// <param name="imageSource">The asset path for the tilesheet texture.</param> + public TilesheetReference(int index, string id, string imageSource) + { + this.Index = index; + this.Id = id; + this.ImageSource = imageSource; + } + } +} diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index 3d5bb29d..27fb3dbb 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -54,6 +54,9 @@ namespace StardewModdingAPI.Framework /// <remarks>The game may adds content managers in asynchronous threads (e.g. when populating the load screen).</remarks> private readonly ReaderWriterLockSlim ContentManagerLock = new ReaderWriterLockSlim(); + /// <summary>A cache of ordered tilesheet IDs used by vanilla maps.</summary> + private readonly IDictionary<string, TilesheetReference[]> VanillaTilesheets = new Dictionary<string, TilesheetReference[]>(StringComparer.OrdinalIgnoreCase); + /// <summary>An unmodified content manager which doesn't intercept assets, used to compare asset data.</summary> private readonly LocalizedContentManager VanillaContentManager; @@ -293,21 +296,21 @@ namespace StardewModdingAPI.Framework }); } - /// <summary>Get a vanilla asset without interception.</summary> - /// <typeparam name="T">The type of asset to load.</typeparam> + /// <summary>Get the tilesheet ID order used by the unmodified version of a map asset.</summary> /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param> - public bool TryLoadVanillaAsset<T>(string assetName, out T asset) + public TilesheetReference[] GetVanillaTilesheetIds(string assetName) { - try + if (!this.VanillaTilesheets.TryGetValue(assetName, out TilesheetReference[] tilesheets)) { - asset = this.VanillaContentManager.Load<T>(assetName); - return true; - } - catch - { - asset = default; - return false; + tilesheets = this.TryLoadVanillaAsset(assetName, out Map map) + ? map.TileSheets.Select((sheet, index) => new TilesheetReference(index, sheet.Id, sheet.ImageSource)).ToArray() + : null; + + this.VanillaTilesheets[assetName] = tilesheets; + this.VanillaContentManager.Unload(); } + + return tilesheets ?? new TilesheetReference[0]; } /// <summary>Dispose held resources.</summary> @@ -341,5 +344,23 @@ namespace StardewModdingAPI.Framework this.ContentManagers.Remove(contentManager) ); } + + /// <summary>Get a vanilla asset without interception.</summary> + /// <typeparam name="T">The type of asset to load.</typeparam> + /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param> + /// <param name="asset">The loaded asset data.</param> + private bool TryLoadVanillaAsset<T>(string assetName, out T asset) + { + try + { + asset = this.VanillaContentManager.Load<T>(assetName); + return true; + } + catch + { + asset = default; + return false; + } + } } } diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index 424d6ff3..3db3856f 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -11,7 +11,6 @@ using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Utilities; using StardewValley; using xTile; -using xTile.Tiles; namespace StardewModdingAPI.Framework.ContentManagers { @@ -398,14 +397,13 @@ namespace StardewModdingAPI.Framework.ContentManagers } // when replacing a map, the vanilla tilesheets must have the same order and IDs - if (data is Map loadedMap && this.Coordinator.TryLoadVanillaAsset(info.AssetName, out Map vanillaMap)) + if (data is Map loadedMap) { - for (int i = 0; i < vanillaMap.TileSheets.Count; i++) + TilesheetReference[] vanillaTilesheetRefs = this.Coordinator.GetVanillaTilesheetIds(info.AssetName); + foreach (TilesheetReference vanillaSheet in vanillaTilesheetRefs) { - // check for match - TileSheet vanillaSheet = vanillaMap.TileSheets[i]; - bool found = this.TryFindTilesheet(loadedMap, vanillaSheet.Id, out int loadedIndex, out TileSheet loadedSheet); - if (found && loadedIndex == i) + // skip if match + if (loadedMap.TileSheets.Count > vanillaSheet.Index && loadedMap.TileSheets[vanillaSheet.Index].Id == vanillaSheet.Id) continue; // handle mismatch @@ -414,18 +412,18 @@ namespace StardewModdingAPI.Framework.ContentManagers // This is temporary: mods shouldn't do this for any vanilla map, but these are the ones we know will crash. Showing a warning for others instead gives modders time to update their mods, while still simplifying troubleshooting. bool isFarmMap = info.AssetNameEquals("Maps/Farm") || info.AssetNameEquals("Maps/Farm_Combat") || info.AssetNameEquals("Maps/Farm_Fishing") || info.AssetNameEquals("Maps/Farm_Foraging") || info.AssetNameEquals("Maps/Farm_FourCorners") || info.AssetNameEquals("Maps/Farm_Island") || info.AssetNameEquals("Maps/Farm_Mining"); - - string reason = found - ? $"mod reordered the original tilesheets, which {(isFarmMap ? "would cause a crash" : "often causes crashes")}.\n\nTechnical details for mod author:\nExpected order [{string.Join(", ", vanillaMap.TileSheets.Select(p => $"'{p.ImageSource}' (id: {p.Id})"))}], but found tilesheet '{vanillaSheet.Id}' at index {loadedIndex} instead of {i}. Make sure custom tilesheet IDs are prefixed with 'z_' to avoid reordering tilesheets." + int loadedIndex = this.TryFindTilesheet(loadedMap, vanillaSheet.Id); + string reason = loadedIndex != -1 + ? $"mod reordered the original tilesheets, which {(isFarmMap ? "would cause a crash" : "often causes crashes")}.\nTechnical details for mod author: Expected order: {string.Join(", ", vanillaTilesheetRefs.Select(p => p.Id))}. See https://stardewcommunitywiki.com/Modding:Maps#Tilesheet_order for help." : $"mod has no tilesheet with ID '{vanillaSheet.Id}'. Map replacements must keep the original tilesheets to avoid errors or crashes."; SCore.DeprecationManager.PlaceholderWarn("3.8.2", DeprecationLevel.PendingRemoval); if (isFarmMap) { - mod.LogAsMod($"SMAPI blocked asset replacement for '{info.AssetName}': {reason}", LogLevel.Error); + mod.LogAsMod($"SMAPI blocked '{info.AssetName}' map load: {reason}", LogLevel.Error); return false; } - mod.LogAsMod($"SMAPI detected a potential issue with asset replacement for '{info.AssetName}' map: {reason}", LogLevel.Warn); + mod.LogAsMod($"SMAPI found an issue with '{info.AssetName}' map load: {reason}", LogLevel.Warn); } } } @@ -436,23 +434,15 @@ namespace StardewModdingAPI.Framework.ContentManagers /// <summary>Find a map tilesheet by ID.</summary> /// <param name="map">The map whose tilesheets to search.</param> /// <param name="id">The tilesheet ID to match.</param> - /// <param name="index">The matched tilesheet index, if any.</param> - /// <param name="tilesheet">The matched tilesheet, if any.</param> - private bool TryFindTilesheet(Map map, string id, out int index, out TileSheet tilesheet) + private int TryFindTilesheet(Map map, string id) { for (int i = 0; i < map.TileSheets.Count; i++) { if (map.TileSheets[i].Id == id) - { - index = i; - tilesheet = map.TileSheets[i]; - return true; - } + return i; } - index = -1; - tilesheet = null; - return false; + return -1; } } } diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 9092669f..665dbfe3 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Reflection; using StardewModdingAPI.Events; -using StardewModdingAPI.Framework.PerformanceMonitoring; namespace StardewModdingAPI.Framework.Events { @@ -178,13 +177,12 @@ namespace StardewModdingAPI.Framework.Events *********/ /// <summary>Construct an instance.</summary> /// <param name="modRegistry">The mod registry with which to identify mods.</param> - /// <param name="performanceMonitor">Tracks performance metrics.</param> - public EventManager(ModRegistry modRegistry, PerformanceMonitor performanceMonitor) + public EventManager(ModRegistry modRegistry) { // create shortcut initializers ManagedEvent<TEventArgs> ManageEventOf<TEventArgs>(string typeName, string eventName, bool isPerformanceCritical = false) { - return new ManagedEvent<TEventArgs>($"{typeName}.{eventName}", modRegistry, performanceMonitor, isPerformanceCritical); + return new ManagedEvent<TEventArgs>($"{typeName}.{eventName}", modRegistry, isPerformanceCritical); } // init events (new) diff --git a/src/SMAPI/Framework/Events/ManagedEvent.cs b/src/SMAPI/Framework/Events/ManagedEvent.cs index f2dfb2ab..2204966c 100644 --- a/src/SMAPI/Framework/Events/ManagedEvent.cs +++ b/src/SMAPI/Framework/Events/ManagedEvent.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using StardewModdingAPI.Events; -using StardewModdingAPI.Framework.PerformanceMonitoring; namespace StardewModdingAPI.Framework.Events { @@ -17,9 +16,6 @@ namespace StardewModdingAPI.Framework.Events /// <summary>The mod registry with which to identify mods.</summary> protected readonly ModRegistry ModRegistry; - /// <summary>Tracks performance metrics.</summary> - private readonly PerformanceMonitor PerformanceMonitor; - /// <summary>The underlying event handlers.</summary> private readonly List<ManagedEventHandler<TEventArgs>> Handlers = new List<ManagedEventHandler<TEventArgs>>(); @@ -49,13 +45,11 @@ namespace StardewModdingAPI.Framework.Events /// <summary>Construct an instance.</summary> /// <param name="eventName">A human-readable name for the event.</param> /// <param name="modRegistry">The mod registry with which to identify mods.</param> - /// <param name="performanceMonitor">Tracks performance metrics.</param> /// <param name="isPerformanceCritical">Whether the event is typically called at least once per second.</param> - public ManagedEvent(string eventName, ModRegistry modRegistry, PerformanceMonitor performanceMonitor, bool isPerformanceCritical = false) + public ManagedEvent(string eventName, ModRegistry modRegistry, bool isPerformanceCritical = false) { this.EventName = eventName; this.ModRegistry = modRegistry; - this.PerformanceMonitor = performanceMonitor; this.IsPerformanceCritical = isPerformanceCritical; } @@ -126,40 +120,26 @@ namespace StardewModdingAPI.Framework.Events } // raise event - this.PerformanceMonitor.Track(this.EventName, () => + foreach (ManagedEventHandler<TEventArgs> handler in handlers) { - foreach (ManagedEventHandler<TEventArgs> handler in handlers) - { - if (match != null && !match(handler.SourceMod)) - continue; + if (match != null && !match(handler.SourceMod)) + continue; - try - { - this.PerformanceMonitor.Track(this.EventName, this.GetModNameForPerformanceCounters(handler), () => handler.Handler.Invoke(null, args)); - } - catch (Exception ex) - { - this.LogError(handler, ex); - } + try + { + handler.Handler.Invoke(null, args); + } + catch (Exception ex) + { + this.LogError(handler, ex); } - }); + } } /********* ** Private methods *********/ - /// <summary>Get the mod name for a given event handler to display in performance monitoring reports.</summary> - /// <param name="handler">The event handler.</param> - private string GetModNameForPerformanceCounters(ManagedEventHandler<TEventArgs> handler) - { - IModMetadata mod = handler.SourceMod; - - return mod.HasManifest() - ? mod.Manifest.UniqueID - : mod.DisplayName; - } - /// <summary>Log an exception from an event handler.</summary> /// <param name="handler">The event handler instance.</param> /// <param name="ex">The exception that was raised.</param> diff --git a/src/SMAPI/Framework/PerformanceMonitoring/AlertContext.cs b/src/SMAPI/Framework/PerformanceMonitoring/AlertContext.cs deleted file mode 100644 index af630055..00000000 --- a/src/SMAPI/Framework/PerformanceMonitoring/AlertContext.cs +++ /dev/null @@ -1,34 +0,0 @@ -namespace StardewModdingAPI.Framework.PerformanceMonitoring -{ - /// <summary>The context for an alert.</summary> - internal readonly struct AlertContext - { - /********* - ** Accessors - *********/ - /// <summary>The source which triggered the alert.</summary> - public string Source { get; } - - /// <summary>The elapsed milliseconds.</summary> - public double Elapsed { get; } - - - /********* - ** Public methods - *********/ - /// <summary>Construct an instance.</summary> - /// <param name="source">The source which triggered the alert.</param> - /// <param name="elapsed">The elapsed milliseconds.</param> - public AlertContext(string source, double elapsed) - { - this.Source = source; - this.Elapsed = elapsed; - } - - /// <summary>Get a human-readable text form of this instance.</summary> - public override string ToString() - { - return $"{this.Source}: {this.Elapsed:F2}ms"; - } - } -} diff --git a/src/SMAPI/Framework/PerformanceMonitoring/AlertEntry.cs b/src/SMAPI/Framework/PerformanceMonitoring/AlertEntry.cs deleted file mode 100644 index d5a0b343..00000000 --- a/src/SMAPI/Framework/PerformanceMonitoring/AlertEntry.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace StardewModdingAPI.Framework.PerformanceMonitoring -{ - /// <summary>A single alert entry.</summary> - internal readonly struct AlertEntry - { - /********* - ** Accessors - *********/ - /// <summary>The collection in which the alert occurred.</summary> - public PerformanceCounterCollection Collection { get; } - - /// <summary>The actual execution time in milliseconds.</summary> - public double ExecutionTimeMilliseconds { get; } - - /// <summary>The configured alert threshold in milliseconds.</summary> - public double ThresholdMilliseconds { get; } - - /// <summary>The sources involved in exceeding the threshold.</summary> - public AlertContext[] Context { get; } - - - /********* - ** Public methods - *********/ - /// <summary>Construct an instance.</summary> - /// <param name="collection">The collection in which the alert occurred.</param> - /// <param name="executionTimeMilliseconds">The actual execution time in milliseconds.</param> - /// <param name="thresholdMilliseconds">The configured alert threshold in milliseconds.</param> - /// <param name="context">The sources involved in exceeding the threshold.</param> - public AlertEntry(PerformanceCounterCollection collection, double executionTimeMilliseconds, double thresholdMilliseconds, AlertContext[] context) - { - this.Collection = collection; - this.ExecutionTimeMilliseconds = executionTimeMilliseconds; - this.ThresholdMilliseconds = thresholdMilliseconds; - this.Context = context; - } - } -} diff --git a/src/SMAPI/Framework/PerformanceMonitoring/PeakEntry.cs b/src/SMAPI/Framework/PerformanceMonitoring/PeakEntry.cs deleted file mode 100644 index 1746e358..00000000 --- a/src/SMAPI/Framework/PerformanceMonitoring/PeakEntry.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; - -namespace StardewModdingAPI.Framework.PerformanceMonitoring -{ - /// <summary>A peak invocation time.</summary> - internal readonly struct PeakEntry - { - /********* - ** Accessors - *********/ - /// <summary>The actual execution time in milliseconds.</summary> - public double ExecutionTimeMilliseconds { get; } - - /// <summary>When the entry occurred.</summary> - public DateTime EventTime { get; } - - /// <summary>The sources involved in exceeding the threshold.</summary> - public AlertContext[] Context { get; } - - - /********* - ** Public methods - *********/ - /// <summary>Construct an instance.</summary> - /// <param name="executionTimeMilliseconds">The actual execution time in milliseconds.</param> - /// <param name="eventTime">When the entry occurred.</param> - /// <param name="context">The sources involved in exceeding the threshold.</param> - public PeakEntry(double executionTimeMilliseconds, DateTime eventTime, AlertContext[] context) - { - this.ExecutionTimeMilliseconds = executionTimeMilliseconds; - this.EventTime = eventTime; - this.Context = context; - } - } -} diff --git a/src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounter.cs b/src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounter.cs deleted file mode 100644 index 42825999..00000000 --- a/src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounter.cs +++ /dev/null @@ -1,124 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace StardewModdingAPI.Framework.PerformanceMonitoring -{ - /// <summary>Tracks metadata about a particular code event.</summary> - internal class PerformanceCounter - { - /********* - ** Fields - *********/ - /// <summary>The size of the ring buffer.</summary> - private readonly int MaxEntries = 16384; - - /// <summary>The collection to which this performance counter belongs.</summary> - private readonly PerformanceCounterCollection ParentCollection; - - /// <summary>The performance counter entries.</summary> - private readonly Stack<PerformanceCounterEntry> Entries; - - /// <summary>The entry with the highest execution time.</summary> - private PerformanceCounterEntry? PeakPerformanceCounterEntry; - - - /********* - ** Accessors - *********/ - /// <summary>The name of the source.</summary> - public string Source { get; } - - /// <summary>The alert threshold in milliseconds</summary> - public double AlertThresholdMilliseconds { get; set; } - - /// <summary>If alerting is enabled or not</summary> - public bool EnableAlerts { get; set; } - - - /********* - ** Public methods - *********/ - /// <summary>Construct an instance.</summary> - /// <param name="parentCollection">The collection to which this performance counter belongs.</param> - /// <param name="source">The name of the source.</param> - public PerformanceCounter(PerformanceCounterCollection parentCollection, string source) - { - this.ParentCollection = parentCollection; - this.Source = source; - this.Entries = new Stack<PerformanceCounterEntry>(this.MaxEntries); - } - - /// <summary>Add a performance counter entry to the list, update monitoring, and raise alerts if needed.</summary> - /// <param name="entry">The entry to add.</param> - public void Add(PerformanceCounterEntry entry) - { - // add entry - if (this.Entries.Count > this.MaxEntries) - this.Entries.Pop(); - this.Entries.Push(entry); - - // update metrics - if (this.PeakPerformanceCounterEntry == null || entry.ElapsedMilliseconds > this.PeakPerformanceCounterEntry.Value.ElapsedMilliseconds) - this.PeakPerformanceCounterEntry = entry; - - // raise alert - if (this.EnableAlerts && entry.ElapsedMilliseconds > this.AlertThresholdMilliseconds) - this.ParentCollection.AddAlert(entry.ElapsedMilliseconds, this.AlertThresholdMilliseconds, new AlertContext(this.Source, entry.ElapsedMilliseconds)); - } - - /// <summary>Clear all performance counter entries and monitoring.</summary> - public void Reset() - { - this.Entries.Clear(); - this.PeakPerformanceCounterEntry = null; - } - - /// <summary>Get the peak entry.</summary> - public PerformanceCounterEntry? GetPeak() - { - return this.PeakPerformanceCounterEntry; - } - - /// <summary>Get the entry with the highest execution time.</summary> - /// <param name="range">The time range to search.</param> - /// <param name="endTime">The end time for the <paramref name="range"/>, or null for the current time.</param> - public PerformanceCounterEntry? GetPeak(TimeSpan range, DateTime? endTime = null) - { - endTime ??= DateTime.UtcNow; - DateTime startTime = endTime.Value.Subtract(range); - - return this.Entries - .Where(entry => entry.EventTime >= startTime && entry.EventTime <= endTime) - .OrderByDescending(x => x.ElapsedMilliseconds) - .FirstOrDefault(); - } - - /// <summary>Get the last entry added to the list.</summary> - public PerformanceCounterEntry? GetLastEntry() - { - if (this.Entries.Count == 0) - return null; - - return this.Entries.Peek(); - } - - /// <summary>Get the average over a given time span.</summary> - /// <param name="range">The time range to search.</param> - /// <param name="endTime">The end time for the <paramref name="range"/>, or null for the current time.</param> - public double GetAverage(TimeSpan range, DateTime? endTime = null) - { - endTime ??= DateTime.UtcNow; - DateTime startTime = endTime.Value.Subtract(range); - - double[] entries = this.Entries - .Where(entry => entry.EventTime >= startTime && entry.EventTime <= endTime) - .Select(p => p.ElapsedMilliseconds) - .ToArray(); - - return entries.Length > 0 - ? entries.Average() - : 0; - } - } -} diff --git a/src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounterCollection.cs b/src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounterCollection.cs deleted file mode 100644 index 29a06794..00000000 --- a/src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounterCollection.cs +++ /dev/null @@ -1,205 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; - -namespace StardewModdingAPI.Framework.PerformanceMonitoring -{ - internal class PerformanceCounterCollection - { - /********* - ** Fields - *********/ - /// <summary>The number of peak invocations to keep.</summary> - private readonly int MaxEntries = 16384; - - /// <summary>The sources involved in exceeding alert thresholds.</summary> - private readonly List<AlertContext> TriggeredPerformanceCounters = new List<AlertContext>(); - - /// <summary>The stopwatch used to track the invocation time.</summary> - private readonly Stopwatch InvocationStopwatch = new Stopwatch(); - - /// <summary>The performance counter manager.</summary> - private readonly PerformanceMonitor PerformanceMonitor; - - /// <summary>The time to calculate average calls per second.</summary> - private DateTime CallsPerSecondStart = DateTime.UtcNow; - - /// <summary>The number of invocations.</summary> - private long CallCount; - - /// <summary>The peak invocations.</summary> - private readonly Stack<PeakEntry> PeakInvocations; - - - /********* - ** Accessors - *********/ - /// <summary>The associated performance counters.</summary> - public IDictionary<string, PerformanceCounter> PerformanceCounters { get; } = new Dictionary<string, PerformanceCounter>(); - - /// <summary>The name of this collection.</summary> - public string Name { get; } - - /// <summary>Whether the source is typically invoked at least once per second.</summary> - public bool IsPerformanceCritical { get; } - - /// <summary>The alert threshold in milliseconds.</summary> - public double AlertThresholdMilliseconds { get; set; } - - /// <summary>Whether alerts are enabled.</summary> - public bool EnableAlerts { get; set; } - - - /********* - ** Public methods - *********/ - /// <summary>Construct an instance.</summary> - /// <param name="performanceMonitor">The performance counter manager.</param> - /// <param name="name">The name of this collection.</param> - /// <param name="isPerformanceCritical">Whether the source is typically invoked at least once per second.</param> - public PerformanceCounterCollection(PerformanceMonitor performanceMonitor, string name, bool isPerformanceCritical = false) - { - this.PeakInvocations = new Stack<PeakEntry>(this.MaxEntries); - this.Name = name; - this.PerformanceMonitor = performanceMonitor; - this.IsPerformanceCritical = isPerformanceCritical; - } - - /// <summary>Track a single invocation for a named source.</summary> - /// <param name="source">The name of the source.</param> - /// <param name="entry">The entry.</param> - public void Track(string source, PerformanceCounterEntry entry) - { - // add entry - if (!this.PerformanceCounters.ContainsKey(source)) - this.PerformanceCounters.Add(source, new PerformanceCounter(this, source)); - this.PerformanceCounters[source].Add(entry); - - // raise alert - if (this.EnableAlerts) - this.TriggeredPerformanceCounters.Add(new AlertContext(source, entry.ElapsedMilliseconds)); - } - - /// <summary>Get the average execution time for all non-game internal sources in milliseconds.</summary> - /// <param name="interval">The interval for which to get the average, relative to now</param> - public double GetModsAverageExecutionTime(TimeSpan interval) - { - return this.PerformanceCounters - .Where(entry => entry.Key != Constants.GamePerformanceCounterName) - .Sum(entry => entry.Value.GetAverage(interval)); - } - - /// <summary>Get the overall average execution time in milliseconds.</summary> - /// <param name="interval">The interval for which to get the average, relative to now</param> - public double GetAverageExecutionTime(TimeSpan interval) - { - return this.PerformanceCounters - .Sum(entry => entry.Value.GetAverage(interval)); - } - - /// <summary>Get the average execution time for game-internal sources in milliseconds.</summary> - public double GetGameAverageExecutionTime(TimeSpan interval) - { - return this.PerformanceCounters.TryGetValue(Constants.GamePerformanceCounterName, out PerformanceCounter gameExecTime) - ? gameExecTime.GetAverage(interval) - : 0; - } - - /// <summary>Get the peak execution time in milliseconds.</summary> - /// <param name="range">The time range to search.</param> - /// <param name="endTime">The end time for the <paramref name="range"/>, or null for the current time.</param> - public double GetPeakExecutionTime(TimeSpan range, DateTime? endTime = null) - { - if (this.PeakInvocations.Count == 0) - return 0; - - endTime ??= DateTime.UtcNow; - DateTime startTime = endTime.Value.Subtract(range); - - return this.PeakInvocations - .Where(entry => entry.EventTime >= startTime && entry.EventTime <= endTime) - .OrderByDescending(x => x.ExecutionTimeMilliseconds) - .Select(p => p.ExecutionTimeMilliseconds) - .FirstOrDefault(); - } - - /// <summary>Start tracking the invocation of this collection.</summary> - public void BeginTrackInvocation() - { - this.TriggeredPerformanceCounters.Clear(); - this.InvocationStopwatch.Reset(); - this.InvocationStopwatch.Start(); - - this.CallCount++; - } - - /// <summary>End tracking the invocation of this collection, and raise an alert if needed.</summary> - public void EndTrackInvocation() - { - this.InvocationStopwatch.Stop(); - - // add invocation - if (this.PeakInvocations.Count >= this.MaxEntries) - this.PeakInvocations.Pop(); - this.PeakInvocations.Push(new PeakEntry(this.InvocationStopwatch.Elapsed.TotalMilliseconds, DateTime.UtcNow, this.TriggeredPerformanceCounters.ToArray())); - - // raise alert - if (this.EnableAlerts && this.InvocationStopwatch.Elapsed.TotalMilliseconds >= this.AlertThresholdMilliseconds) - this.AddAlert(this.InvocationStopwatch.Elapsed.TotalMilliseconds, this.AlertThresholdMilliseconds, this.TriggeredPerformanceCounters.ToArray()); - } - - /// <summary>Add an alert.</summary> - /// <param name="executionTimeMilliseconds">The execution time in milliseconds.</param> - /// <param name="thresholdMilliseconds">The configured threshold.</param> - /// <param name="alerts">The sources involved in exceeding the threshold.</param> - public void AddAlert(double executionTimeMilliseconds, double thresholdMilliseconds, AlertContext[] alerts) - { - this.PerformanceMonitor.AddAlert( - new AlertEntry(this, executionTimeMilliseconds, thresholdMilliseconds, alerts) - ); - } - - /// <summary>Add an alert.</summary> - /// <param name="executionTimeMilliseconds">The execution time in milliseconds.</param> - /// <param name="thresholdMilliseconds">The configured threshold.</param> - /// <param name="alert">The source involved in exceeding the threshold.</param> - public void AddAlert(double executionTimeMilliseconds, double thresholdMilliseconds, AlertContext alert) - { - this.AddAlert(executionTimeMilliseconds, thresholdMilliseconds, new[] { alert }); - } - - /// <summary>Reset the calls per second counter.</summary> - public void ResetCallsPerSecond() - { - this.CallCount = 0; - this.CallsPerSecondStart = DateTime.UtcNow; - } - - /// <summary>Reset all performance counters in this collection.</summary> - public void Reset() - { - this.PeakInvocations.Clear(); - foreach (var counter in this.PerformanceCounters) - counter.Value.Reset(); - } - - /// <summary>Reset the performance counter for a specific source.</summary> - /// <param name="source">The source name.</param> - public void ResetSource(string source) - { - foreach (var i in this.PerformanceCounters) - if (i.Value.Source.Equals(source, StringComparison.OrdinalIgnoreCase)) - i.Value.Reset(); - } - - /// <summary>Get the average calls per second.</summary> - public long GetAverageCallsPerSecond() - { - long runtimeInSeconds = (long)DateTime.UtcNow.Subtract(this.CallsPerSecondStart).TotalSeconds; - return runtimeInSeconds > 0 - ? this.CallCount / runtimeInSeconds - : 0; - } - } -} diff --git a/src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounterEntry.cs b/src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounterEntry.cs deleted file mode 100644 index 18cca628..00000000 --- a/src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounterEntry.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; - -namespace StardewModdingAPI.Framework.PerformanceMonitoring -{ - /// <summary>A single performance counter entry.</summary> - internal readonly struct PerformanceCounterEntry - { - /********* - ** Accessors - *********/ - /// <summary>When the entry occurred.</summary> - public DateTime EventTime { get; } - - /// <summary>The elapsed milliseconds.</summary> - public double ElapsedMilliseconds { get; } - - - /********* - ** Public methods - *********/ - /// <summary>Construct an instance.</summary> - /// <param name="eventTime">When the entry occurred.</param> - /// <param name="elapsedMilliseconds">The elapsed milliseconds.</param> - public PerformanceCounterEntry(DateTime eventTime, double elapsedMilliseconds) - { - this.EventTime = eventTime; - this.ElapsedMilliseconds = elapsedMilliseconds; - } - } -} diff --git a/src/SMAPI/Framework/PerformanceMonitoring/PerformanceMonitor.cs b/src/SMAPI/Framework/PerformanceMonitoring/PerformanceMonitor.cs deleted file mode 100644 index 3f2608aa..00000000 --- a/src/SMAPI/Framework/PerformanceMonitoring/PerformanceMonitor.cs +++ /dev/null @@ -1,184 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Text; -using StardewModdingAPI.Framework.Events; - -namespace StardewModdingAPI.Framework.PerformanceMonitoring -{ - /// <summary>Tracks performance metrics.</summary> - internal class PerformanceMonitor - { - /********* - ** Fields - *********/ - /// <summary>The recorded alerts.</summary> - private readonly IList<AlertEntry> Alerts = new List<AlertEntry>(); - - /// <summary>The monitor for output logging.</summary> - private readonly IMonitor Monitor; - - /// <summary>The invocation stopwatch.</summary> - private readonly Stopwatch InvocationStopwatch = new Stopwatch(); - - /// <summary>The underlying performance counter collections.</summary> - private readonly IDictionary<string, PerformanceCounterCollection> Collections = new Dictionary<string, PerformanceCounterCollection>(StringComparer.OrdinalIgnoreCase); - - - /********* - ** Accessors - *********/ - /// <summary>Whether alerts are paused.</summary> - public bool PauseAlerts { get; set; } - - /// <summary>Whether performance counter tracking is enabled.</summary> - public bool EnableTracking { get; set; } - - - /********* - ** Public methods - *********/ - /// <summary>Construct an instance.</summary> - /// <param name="monitor">The monitor for output logging.</param> - public PerformanceMonitor(IMonitor monitor) - { - this.Monitor = monitor; - } - - /// <summary>Reset all performance counters in all collections.</summary> - public void Reset() - { - foreach (PerformanceCounterCollection collection in this.Collections.Values) - collection.Reset(); - } - - /// <summary>Track the invocation time for a collection.</summary> - /// <param name="collectionName">The name of the collection.</param> - /// <param name="action">The action to execute and track.</param> - public void Track(string collectionName, Action action) - { - if (!this.EnableTracking) - { - action(); - return; - } - - PerformanceCounterCollection collection = this.GetOrCreateCollectionByName(collectionName); - collection.BeginTrackInvocation(); - try - { - action(); - } - finally - { - collection.EndTrackInvocation(); - } - } - - /// <summary>Track a single performance counter invocation in a specific collection.</summary> - /// <param name="collectionName">The name of the collection.</param> - /// <param name="sourceName">The name of the source.</param> - /// <param name="action">The action to execute and track.</param> - public void Track(string collectionName, string sourceName, Action action) - { - if (!this.EnableTracking) - { - action(); - return; - } - - PerformanceCounterCollection collection = this.GetOrCreateCollectionByName(collectionName); - DateTime eventTime = DateTime.UtcNow; - this.InvocationStopwatch.Reset(); - this.InvocationStopwatch.Start(); - - try - { - action(); - } - finally - { - this.InvocationStopwatch.Stop(); - collection.Track(sourceName, new PerformanceCounterEntry(eventTime, this.InvocationStopwatch.Elapsed.TotalMilliseconds)); - } - } - - /// <summary>Reset the performance counters for a specific collection.</summary> - /// <param name="name">The collection name.</param> - public void ResetCollection(string name) - { - if (this.Collections.TryGetValue(name, out PerformanceCounterCollection collection)) - { - collection.ResetCallsPerSecond(); - collection.Reset(); - } - } - - /// <summary>Reset performance counters for a specific source.</summary> - /// <param name="name">The name of the source.</param> - public void ResetSource(string name) - { - foreach (PerformanceCounterCollection performanceCounterCollection in this.Collections.Values) - performanceCounterCollection.ResetSource(name); - } - - /// <summary>Print any queued alerts.</summary> - public void PrintQueuedAlerts() - { - if (this.Alerts.Count == 0) - return; - - StringBuilder report = new StringBuilder(); - - foreach (AlertEntry alert in this.Alerts) - { - report.AppendLine($"{alert.Collection.Name} took {alert.ExecutionTimeMilliseconds:F2}ms (exceeded threshold of {alert.ThresholdMilliseconds:F2}ms)"); - - foreach (AlertContext context in alert.Context.OrderByDescending(p => p.Elapsed)) - report.AppendLine(context.ToString()); - } - - this.Alerts.Clear(); - this.Monitor.Log(report.ToString(), LogLevel.Error); - } - - /// <summary>Add an alert to the queue.</summary> - /// <param name="entry">The alert to add.</param> - public void AddAlert(AlertEntry entry) - { - if (!this.PauseAlerts) - this.Alerts.Add(entry); - } - - /// <summary>Initialize the default performance counter collections.</summary> - /// <param name="eventManager">The event manager.</param> - public void InitializePerformanceCounterCollections(EventManager eventManager) - { - foreach (IManagedEvent @event in eventManager.GetAllEvents()) - this.Collections[@event.EventName] = new PerformanceCounterCollection(this, @event.EventName, @event.IsPerformanceCritical); - } - - /// <summary>Get the underlying performance counters.</summary> - public IEnumerable<PerformanceCounterCollection> GetCollections() - { - return this.Collections.Values; - } - - - /********* - ** Public methods - *********/ - /// <summary>Get a collection by name and creates it if it doesn't exist.</summary> - /// <param name="name">The name of the collection.</param> - private PerformanceCounterCollection GetOrCreateCollectionByName(string name) - { - if (!this.Collections.TryGetValue(name, out PerformanceCounterCollection collection)) - { - collection = new PerformanceCounterCollection(this, name); - this.Collections[name] = collection; - } - return collection; - } - } -} diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 5dc33828..3a51b418 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -28,7 +28,6 @@ using StardewModdingAPI.Framework.ModHelpers; using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Framework.Networking; using StardewModdingAPI.Framework.Patching; -using StardewModdingAPI.Framework.PerformanceMonitoring; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Rendering; using StardewModdingAPI.Framework.Serialization; @@ -82,7 +81,7 @@ namespace StardewModdingAPI.Framework ** Higher-level components ****/ /// <summary>Manages console commands.</summary> - private readonly CommandManager CommandManager = new CommandManager(); + private readonly CommandManager CommandManager; /// <summary>The underlying game instance.</summary> private SGameRunner Game; @@ -131,9 +130,12 @@ namespace StardewModdingAPI.Framework /// <summary>Asset interceptors added or removed since the last tick.</summary> private readonly List<AssetInterceptorChange> ReloadAssetInterceptorsQueue = new List<AssetInterceptorChange>(); - /// <summary>A list of queued commands to execute.</summary> + /// <summary>A list of queued commands to parse and execute.</summary> /// <remarks>This property must be thread-safe, since it's accessed from a separate console input thread.</remarks> - public ConcurrentQueue<string> CommandQueue { get; } = new ConcurrentQueue<string>(); + private readonly ConcurrentQueue<string> RawCommandQueue = new ConcurrentQueue<string>(); + + /// <summary>A list of commands to execute on each screen.</summary> + private readonly PerScreen<List<Tuple<Command, string, string[]>>> ScreenCommandQueue = new(() => new()); /********* @@ -143,10 +145,6 @@ namespace StardewModdingAPI.Framework /// <remarks>This is initialized after the game starts. This is accessed directly because it's not part of the normal class model.</remarks> internal static DeprecationManager DeprecationManager { get; private set; } - /// <summary>Manages performance counters.</summary> - /// <remarks>This is initialized after the game starts. This is non-private for use by Console Commands.</remarks> - internal static PerformanceMonitor PerformanceMonitor { get; private set; } - /// <summary>The number of update ticks which have already executed. This is similar to <see cref="Game1.ticks"/>, but incremented more consistently for every tick.</summary> internal static uint TicksElapsed { get; private set; } @@ -174,13 +172,9 @@ namespace StardewModdingAPI.Framework JsonConvert.PopulateObject(File.ReadAllText(Constants.ApiUserConfigPath), this.Settings); this.LogManager = new LogManager(logPath: logPath, colorConfig: this.Settings.ConsoleColors, writeToConsole: writeToConsole, isVerbose: this.Settings.VerboseLogging, isDeveloperMode: this.Settings.DeveloperMode, getScreenIdForLog: this.GetScreenIdForLog); - - SCore.PerformanceMonitor = new PerformanceMonitor(this.Monitor); - this.EventManager = new EventManager(this.ModRegistry, SCore.PerformanceMonitor); - SCore.PerformanceMonitor.InitializePerformanceCounterCollections(this.EventManager); - + this.CommandManager = new CommandManager(this.Monitor); + this.EventManager = new EventManager(this.ModRegistry); SCore.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry); - SDate.Translations = this.Translator; // log SMAPI/OS info @@ -413,7 +407,7 @@ namespace StardewModdingAPI.Framework () => this.LogManager.RunConsoleInputLoop( commandManager: this.CommandManager, reloadTranslations: this.ReloadTranslations, - handleInput: input => this.CommandQueue.Enqueue(input), + handleInput: input => this.RawCommandQueue.Enqueue(input), continueWhile: () => this.IsGameRunning && !this.CancellationToken.IsCancellationRequested ) ).Start(); @@ -443,7 +437,6 @@ namespace StardewModdingAPI.Framework *********/ // print warnings/alerts SCore.DeprecationManager.PrintQueued(); - SCore.PerformanceMonitor.PrintQueuedAlerts(); /********* ** First-tick initialization @@ -497,17 +490,18 @@ namespace StardewModdingAPI.Framework } /********* - ** Execute commands + ** Parse commands *********/ - while (this.CommandQueue.TryDequeue(out string rawInput)) + while (this.RawCommandQueue.TryDequeue(out string rawInput)) { // parse command string name; string[] args; Command command; + int screenId; try { - if (!this.CommandManager.TryParse(rawInput, out name, out args, out command)) + if (!this.CommandManager.TryParse(rawInput, out name, out args, out command, out screenId)) { this.Monitor.Log("Unknown command; type 'help' for a list of available commands.", LogLevel.Error); continue; @@ -519,18 +513,8 @@ namespace StardewModdingAPI.Framework continue; } - // execute command - try - { - command.Callback.Invoke(name, args); - } - catch (Exception ex) - { - if (command.Mod != null) - command.Mod.LogAsMod($"Mod failed handling that command:\n{ex.GetLogSummary()}", LogLevel.Error); - else - this.Monitor.Log($"Failed handling that command:\n{ex.GetLogSummary()}", LogLevel.Error); - } + // queue command for screen + this.ScreenCommandQueue.GetValueForScreen(screenId).Add(Tuple.Create(command, name, args)); } /********* @@ -578,7 +562,9 @@ namespace StardewModdingAPI.Framework try { - // reapply overrides + /********* + ** Reapply overrides + *********/ if (this.JustReturnedToTitle) { if (!(Game1.mapDisplayDevice is SDisplayDevice)) @@ -588,6 +574,33 @@ namespace StardewModdingAPI.Framework } /********* + ** Execute commands + *********/ + { + var commandQueue = this.ScreenCommandQueue.Value; + foreach (var entry in commandQueue) + { + Command command = entry.Item1; + string name = entry.Item2; + string[] args = entry.Item3; + + try + { + command.Callback.Invoke(name, args); + } + catch (Exception ex) + { + if (command.Mod != null) + command.Mod.LogAsMod($"Mod failed handling that command:\n{ex.GetLogSummary()}", LogLevel.Error); + else + this.Monitor.Log($"Failed handling that command:\n{ex.GetLogSummary()}", LogLevel.Error); + } + } + commandQueue.Clear(); + } + + + /********* ** Update input *********/ // This should *always* run, even when suppressing mod events, since the game uses diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index f69c5f08..2ede40ae 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -247,7 +247,7 @@ namespace StardewModdingAPI.Framework } if (target_screen != null) { - base.GraphicsDevice.SetRenderTarget(target_screen); + Game1.SetRenderTarget(target_screen); } if (this.IsSaving) { @@ -352,8 +352,7 @@ namespace StardewModdingAPI.Framework events.Rendered.RaiseEmpty(); Game1.spriteBatch.End(); } - - base.GraphicsDevice.SetRenderTarget(target_screen); + Game1.SetRenderTarget(target_screen); return; } if (Game1.showingEndOfNightStuff) @@ -426,7 +425,7 @@ namespace StardewModdingAPI.Framework } if (Game1.drawLighting) { - base.GraphicsDevice.SetRenderTarget(Game1.lightmap); + Game1.SetRenderTarget(Game1.lightmap); base.GraphicsDevice.Clear(Microsoft.Xna.Framework.Color.White * 0f); Matrix lighting_matrix = Matrix.Identity; if (this.useUnscaledLighting) @@ -473,7 +472,7 @@ namespace StardewModdingAPI.Framework } } Game1.spriteBatch.End(); - base.GraphicsDevice.SetRenderTarget(target_screen); + Game1.SetRenderTarget(target_screen); } if (Game1.bloomDay && Game1.bloom != null) { diff --git a/src/SMAPI/Properties/AssemblyInfo.cs b/src/SMAPI/Properties/AssemblyInfo.cs index f8f7f4ea..ee8a1674 100644 --- a/src/SMAPI/Properties/AssemblyInfo.cs +++ b/src/SMAPI/Properties/AssemblyInfo.cs @@ -1,5 +1,4 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("SMAPI.Tests")] -[assembly: InternalsVisibleTo("ConsoleCommands")] // for performance monitoring commands [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // Moq for unit testing diff --git a/src/SMAPI/Utilities/PerScreen.cs b/src/SMAPI/Utilities/PerScreen.cs index 55dae0d8..89d08e87 100644 --- a/src/SMAPI/Utilities/PerScreen.cs +++ b/src/SMAPI/Utilities/PerScreen.cs @@ -28,18 +28,8 @@ namespace StardewModdingAPI.Utilities /// <remarks>The value is initialized the first time it's requested for that player, unless it's set manually first.</remarks> public T Value { - get - { - this.RemoveDeadPlayers(); - return this.States.TryGetValue(Context.ScreenId, out T state) - ? state - : this.States[Context.ScreenId] = this.CreateNewState(); - } - set - { - this.RemoveDeadPlayers(); - this.States[Context.ScreenId] = value; - } + get => this.GetValueForScreen(Context.ScreenId); + set => this.SetValueForScreen(Context.ScreenId, value); } @@ -57,6 +47,25 @@ namespace StardewModdingAPI.Utilities this.CreateNewState = createNewState ?? (() => default); } + /// <summary>Get the value for a given screen ID, creating it if needed.</summary> + /// <param name="screenId">The screen ID to check.</param> + internal T GetValueForScreen(int screenId) + { + this.RemoveDeadPlayers(); + return this.States.TryGetValue(screenId, out T state) + ? state + : this.States[screenId] = this.CreateNewState(); + } + + /// <summary>Set the value for a given screen ID, creating it if needed.</summary> + /// <param name="screenId">The screen ID whose value set.</param> + /// <param name="value">The value to set.</param> + internal void SetValueForScreen(int screenId, T value) + { + this.RemoveDeadPlayers(); + this.States[screenId] = value; + } + /********* ** Private methods |