summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJesse Plamondon-Willard <Pathoschild@users.noreply.github.com>2021-01-08 21:18:15 -0500
committerJesse Plamondon-Willard <Pathoschild@users.noreply.github.com>2021-01-08 21:18:15 -0500
commitbdb7b04b3edecdf7dfa665d8b5a50a1bbff62699 (patch)
tree7249d86910ce37a93a82db8322fade5465445dc4
parent04c6733adae9ce568aefb5d9dee6101097e994c5 (diff)
parentdf25368300e1254bc6eb2a1c7aa371bfeb289dff (diff)
downloadSMAPI-bdb7b04b3edecdf7dfa665d8b5a50a1bbff62699.tar.gz
SMAPI-bdb7b04b3edecdf7dfa665d8b5a50a1bbff62699.tar.bz2
SMAPI-bdb7b04b3edecdf7dfa665d8b5a50a1bbff62699.zip
Merge branch 'develop' into stable
-rw-r--r--build/common.targets2
-rw-r--r--docs/release-notes.md15
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs50
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs647
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs5
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/manifest.json4
-rw-r--r--src/SMAPI.Mods.SaveBackup/manifest.json4
-rw-r--r--src/SMAPI.Web/Startup.cs1
-rw-r--r--src/SMAPI/Constants.cs7
-rw-r--r--src/SMAPI/Framework/CommandManager.cs67
-rw-r--r--src/SMAPI/Framework/Commands/HelpCommand.cs18
-rw-r--r--src/SMAPI/Framework/Content/TilesheetReference.cs33
-rw-r--r--src/SMAPI/Framework/ContentCoordinator.cs43
-rw-r--r--src/SMAPI/Framework/ContentManagers/GameContentManager.cs36
-rw-r--r--src/SMAPI/Framework/Events/EventManager.cs6
-rw-r--r--src/SMAPI/Framework/Events/ManagedEvent.cs44
-rw-r--r--src/SMAPI/Framework/PerformanceMonitoring/AlertContext.cs34
-rw-r--r--src/SMAPI/Framework/PerformanceMonitoring/AlertEntry.cs38
-rw-r--r--src/SMAPI/Framework/PerformanceMonitoring/PeakEntry.cs35
-rw-r--r--src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounter.cs124
-rw-r--r--src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounterCollection.cs205
-rw-r--r--src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounterEntry.cs30
-rw-r--r--src/SMAPI/Framework/PerformanceMonitoring/PerformanceMonitor.cs184
-rw-r--r--src/SMAPI/Framework/SCore.cs77
-rw-r--r--src/SMAPI/Framework/SGame.cs9
-rw-r--r--src/SMAPI/Properties/AssemblyInfo.cs1
-rw-r--r--src/SMAPI/Utilities/PerScreen.cs33
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