diff options
author | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2020-02-01 16:21:35 -0500 |
---|---|---|
committer | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2020-02-01 16:21:35 -0500 |
commit | c8d627cdf2ae3126584ec2500877ff19987db17f (patch) | |
tree | 2cc6f604df00027239476acf3a74ae6bb0761323 /src/SMAPI.Mods.ConsoleCommands/Framework/Commands | |
parent | f976b5c0f095a881fc20f6ce5dcf5a50ebb2b5da (diff) | |
parent | 17a9193fd28c527dcba40360702adb277736cc45 (diff) | |
download | SMAPI-c8d627cdf2ae3126584ec2500877ff19987db17f.tar.gz SMAPI-c8d627cdf2ae3126584ec2500877ff19987db17f.tar.bz2 SMAPI-c8d627cdf2ae3126584ec2500877ff19987db17f.zip |
Merge branch 'develop' into stable
Diffstat (limited to 'src/SMAPI.Mods.ConsoleCommands/Framework/Commands')
9 files changed, 805 insertions, 62 deletions
diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs index 10007b42..9c7082c9 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs @@ -1,6 +1,7 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; +using System.Globalization; using System.Linq; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands @@ -31,13 +32,6 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands /// <param name="index">The zero-based index of the element to get.</param> public string this[int index] => this.Args[index]; - /// <summary>A method which parses a string argument into the given value.</summary> - /// <typeparam name="T">The expected argument type.</typeparam> - /// <param name="input">The argument to parse.</param> - /// <param name="output">The parsed value.</param> - /// <returns>Returns whether the argument was successfully parsed.</returns> - public delegate bool ParseDelegate<T>(string input, out T output); - /********* ** Public methods @@ -113,6 +107,38 @@ 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() @@ -154,5 +180,22 @@ 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/ITrainerCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ITrainerCommand.cs index a0b739f8..d4d36e5d 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ITrainerCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ITrainerCommand.cs @@ -12,8 +12,11 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands /// <summary>The command description.</summary> string Description { get; } - /// <summary>Whether the command needs to perform logic when the game updates.</summary> - bool NeedsUpdate { get; } + /// <summary>Whether the command may need to perform logic when the game updates. This value shouldn't change.</summary> + bool MayNeedUpdate { get; } + + /// <summary>Whether the command may need to perform logic when the player presses a button. This value shouldn't change.</summary> + bool MayNeedInput { get; } /********* @@ -27,6 +30,11 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands /// <summary>Perform any logic needed on update tick.</summary> /// <param name="monitor">Writes messages to the console and log file.</param> - void Update(IMonitor monitor); + void OnUpdated(IMonitor monitor); + + /// <summary>Perform any logic when input is received.</summary> + /// <param name="monitor">Writes messages to the console and log file.</param> + /// <param name="button">The button that was pressed.</param> + void OnButtonPressed(IMonitor monitor, SButton button); } } diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs new file mode 100644 index 00000000..63851c9d --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs @@ -0,0 +1,647 @@ +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/Other/TestInputCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/TestInputCommand.cs new file mode 100644 index 00000000..11aa10c3 --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/TestInputCommand.cs @@ -0,0 +1,59 @@ +using System; + +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other +{ + /// <summary>A command which logs the keys being pressed for 30 seconds once enabled.</summary> + internal class TestInputCommand : TrainerCommand + { + /********* + ** Fields + *********/ + /// <summary>The number of seconds for which to log input.</summary> + private readonly int LogSeconds = 30; + + /// <summary>When the command should stop printing input, or <c>null</c> if currently disabled.</summary> + private long? ExpiryTicks; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + public TestInputCommand() + : base("test_input", "Prints all input to the console for 30 seconds.", mayNeedUpdate: true, mayNeedInput: true) { } + + /// <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) + { + this.ExpiryTicks = DateTime.UtcNow.Add(TimeSpan.FromSeconds(this.LogSeconds)).Ticks; + monitor.Log($"OK, logging all player input for {this.LogSeconds} seconds.", LogLevel.Info); + } + + /// <summary>Perform any logic needed on update tick.</summary> + /// <param name="monitor">Writes messages to the console and log file.</param> + public override void OnUpdated(IMonitor monitor) + { + // handle expiry + if (this.ExpiryTicks == null) + return; + if (this.ExpiryTicks <= DateTime.UtcNow.Ticks) + { + monitor.Log("No longer logging input.", LogLevel.Info); + this.ExpiryTicks = null; + return; + } + } + + /// <summary>Perform any logic when input is received.</summary> + /// <param name="monitor">Writes messages to the console and log file.</param> + /// <param name="button">The button that was pressed.</param> + public override void OnButtonPressed(IMonitor monitor, SButton button) + { + if (this.ExpiryTicks != null) + monitor.Log($"Pressed {button}", LogLevel.Info); + } + } +} diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetHealthCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetHealthCommand.cs index 1abb82b5..59bda5dd 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetHealthCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetHealthCommand.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player @@ -14,18 +14,11 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player /********* - ** Accessors - *********/ - /// <summary>Whether the command needs to perform logic when the game updates.</summary> - public override bool NeedsUpdate => this.InfiniteHealth; - - - /********* ** Public methods *********/ /// <summary>Construct an instance.</summary> public SetHealthCommand() - : base("player_sethealth", "Sets the player's health.\n\nUsage: player_sethealth [value]\n- value: an integer amount, or 'inf' for infinite health.") { } + : base("player_sethealth", "Sets the player's health.\n\nUsage: player_sethealth [value]\n- value: an integer amount, or 'inf' for infinite health.", mayNeedUpdate: true) { } /// <summary>Handle the command.</summary> /// <param name="monitor">Writes messages to the console and log file.</param> @@ -62,9 +55,9 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player /// <summary>Perform any logic needed on update tick.</summary> /// <param name="monitor">Writes messages to the console and log file.</param> - public override void Update(IMonitor monitor) + public override void OnUpdated(IMonitor monitor) { - if (this.InfiniteHealth) + if (this.InfiniteHealth && Context.IsWorldReady) Game1.player.health = Game1.player.maxHealth; } } diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMoneyCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMoneyCommand.cs index 1706bbc1..6e3d68b6 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMoneyCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMoneyCommand.cs @@ -14,18 +14,11 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player /********* - ** Accessors - *********/ - /// <summary>Whether the command needs to perform logic when the game updates.</summary> - public override bool NeedsUpdate => this.InfiniteMoney; - - - /********* ** Public methods *********/ /// <summary>Construct an instance.</summary> public SetMoneyCommand() - : base("player_setmoney", "Sets the player's money.\n\nUsage: player_setmoney <value>\n- value: an integer amount, or 'inf' for infinite money.") { } + : base("player_setmoney", "Sets the player's money.\n\nUsage: player_setmoney <value>\n- value: an integer amount, or 'inf' for infinite money.", mayNeedUpdate: true) { } /// <summary>Handle the command.</summary> /// <param name="monitor">Writes messages to the console and log file.</param> @@ -62,9 +55,9 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player /// <summary>Perform any logic needed on update tick.</summary> /// <param name="monitor">Writes messages to the console and log file.</param> - public override void Update(IMonitor monitor) + public override void OnUpdated(IMonitor monitor) { - if (this.InfiniteMoney) + if (this.InfiniteMoney && Context.IsWorldReady) Game1.player.Money = 999999; } } diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStaminaCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStaminaCommand.cs index 009cb1de..60a1dcb1 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStaminaCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStaminaCommand.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player @@ -14,18 +14,11 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player /********* - ** Accessors - *********/ - /// <summary>Whether the command needs to perform logic when the game updates.</summary> - public override bool NeedsUpdate => this.InfiniteStamina; - - - /********* ** Public methods *********/ /// <summary>Construct an instance.</summary> public SetStaminaCommand() - : base("player_setstamina", "Sets the player's stamina.\n\nUsage: player_setstamina [value]\n- value: an integer amount, or 'inf' for infinite stamina.") { } + : base("player_setstamina", "Sets the player's stamina.\n\nUsage: player_setstamina [value]\n- value: an integer amount, or 'inf' for infinite stamina.", mayNeedUpdate: true) { } /// <summary>Handle the command.</summary> /// <param name="monitor">Writes messages to the console and log file.</param> @@ -62,9 +55,9 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player /// <summary>Perform any logic needed on update tick.</summary> /// <param name="monitor">Writes messages to the console and log file.</param> - public override void Update(IMonitor monitor) + public override void OnUpdated(IMonitor monitor) { - if (this.InfiniteStamina) + if (this.InfiniteStamina && Context.IsWorldReady) Game1.player.stamina = Game1.player.MaxStamina; } } diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs index 466b8f6e..77a26c6a 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; @@ -16,8 +16,11 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands /// <summary>The command description.</summary> public string Description { get; } - /// <summary>Whether the command needs to perform logic when the game updates.</summary> - public virtual bool NeedsUpdate { get; } = false; + /// <summary>Whether the command may need to perform logic when the player presses a button. This value shouldn't change.</summary> + public bool MayNeedInput { get; } + + /// <summary>Whether the command may need to perform logic when the game updates. This value shouldn't change.</summary> + public bool MayNeedUpdate { get; } /********* @@ -31,7 +34,12 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands /// <summary>Perform any logic needed on update tick.</summary> /// <param name="monitor">Writes messages to the console and log file.</param> - public virtual void Update(IMonitor monitor) { } + public virtual void OnUpdated(IMonitor monitor) { } + + /// <summary>Perform any logic when input is received.</summary> + /// <param name="monitor">Writes messages to the console and log file.</param> + /// <param name="button">The button that was pressed.</param> + public virtual void OnButtonPressed(IMonitor monitor, SButton button) { } /********* @@ -40,10 +48,14 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands /// <summary>Construct an instance.</summary> /// <param name="name">The command name the user must type.</param> /// <param name="description">The command description.</param> - protected TrainerCommand(string name, string description) + /// <param name="mayNeedInput">Whether the command may need to perform logic when the player presses a button.</param> + /// <param name="mayNeedUpdate">Whether the command may need to perform logic when the game updates.</param> + protected TrainerCommand(string name, string description, bool mayNeedInput = false, bool mayNeedUpdate = false) { this.Name = name; this.Description = description; + this.MayNeedInput = mayNeedInput; + this.MayNeedUpdate = mayNeedUpdate; } /// <summary>Log an error indicating incorrect usage.</summary> @@ -66,7 +78,8 @@ 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> - protected string GetTableString<T>(IEnumerable<T> data, string[] header, Func<T, string[]> getRow) + /// <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) { // get table data int[] widths = header.Select(p => p.Length).ToArray(); @@ -94,8 +107,9 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands return string.Join( Environment.NewLine, - lines.Select(line => string.Join(" | ", line.Select((field, i) => field.PadRight(widths[i], ' ')).ToArray()) - ) + lines.Select(line => string.Join(" | ", + line.Select((field, i) => rightAlign ? field.PadRight(widths[i], ' ') : field.PadLeft(widths[i], ' ')) + )) ); } } diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/FreezeTimeCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/FreezeTimeCommand.cs index 6a7ab162..736a93a0 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/FreezeTimeCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/FreezeTimeCommand.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World @@ -17,18 +17,11 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World /********* - ** Accessors - *********/ - /// <summary>Whether the command needs to perform logic when the game updates.</summary> - public override bool NeedsUpdate => this.FreezeTime; - - - /********* ** Public methods *********/ /// <summary>Construct an instance.</summary> public FreezeTimeCommand() - : base("world_freezetime", "Freezes or resumes time.\n\nUsage: world_freezetime [value]\n- value: one of 0 (resume), 1 (freeze), or blank (toggle).") { } + : base("world_freezetime", "Freezes or resumes time.\n\nUsage: world_freezetime [value]\n- value: one of 0 (resume), 1 (freeze), or blank (toggle).", mayNeedUpdate: true) { } /// <summary>Handle the command.</summary> /// <param name="monitor">Writes messages to the console and log file.</param> @@ -57,9 +50,9 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World /// <summary>Perform any logic needed on update tick.</summary> /// <param name="monitor">Writes messages to the console and log file.</param> - public override void Update(IMonitor monitor) + public override void OnUpdated(IMonitor monitor) { - if (this.FreezeTime) + if (this.FreezeTime && Context.IsWorldReady) Game1.timeOfDay = FreezeTimeCommand.FrozenTime; } } |