Merge branch 'develop' into stable
This commit is contained in:
commit
bdb7b04b3e
|
@ -4,7 +4,7 @@
|
|||
|
||||
<!--set properties -->
|
||||
<PropertyGroup>
|
||||
<Version>3.8.2</Version>
|
||||
<Version>3.8.3</Version>
|
||||
<Product>SMAPI</Product>
|
||||
|
||||
<LangVersion>latest</LangVersion>
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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], ' '))
|
||||
))
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,8 +120,6 @@ namespace StardewModdingAPI.Framework.Events
|
|||
}
|
||||
|
||||
// raise event
|
||||
this.PerformanceMonitor.Track(this.EventName, () =>
|
||||
{
|
||||
foreach (ManagedEventHandler<TEventArgs> handler in handlers)
|
||||
{
|
||||
if (match != null && !match(handler.SourceMod))
|
||||
|
@ -135,31 +127,19 @@ namespace StardewModdingAPI.Framework.Events
|
|||
|
||||
try
|
||||
{
|
||||
this.PerformanceMonitor.Track(this.EventName, this.GetModNameForPerformanceCounters(handler), () => handler.Handler.Invoke(null, args));
|
||||
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>
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
|
@ -587,6 +573,33 @@ namespace StardewModdingAPI.Framework
|
|||
this.JustReturnedToTitle = false;
|
||||
}
|
||||
|
||||
/*********
|
||||
** 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
|
||||
*********/
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue