From c5be446701d4e24a03d8464e9b080ce74d158223 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 6 Jan 2021 00:29:39 -0500 Subject: rework vanilla tilesheet checking to avoid keeping a copy of the vanilla maps in memory --- src/SMAPI/Framework/Content/TilesheetReference.cs | 33 +++++++++++++++++ src/SMAPI/Framework/ContentCoordinator.cs | 43 ++++++++++++++++------ .../ContentManagers/GameContentManager.cs | 32 ++++++---------- 3 files changed, 76 insertions(+), 32 deletions(-) create mode 100644 src/SMAPI/Framework/Content/TilesheetReference.cs (limited to 'src/SMAPI') 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 +{ + /// Basic metadata about a vanilla tilesheet. + internal class TilesheetReference + { + /********* + ** Accessors + *********/ + /// The tilesheet's index in the list. + public readonly int Index; + + /// The tilesheet's unique ID in the map. + public readonly string Id; + + /// The asset path for the tilesheet texture. + public readonly string ImageSource; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The tilesheet's index in the list. + /// The tilesheet's unique ID in the map. + /// The asset path for the tilesheet texture. + 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 /// The game may adds content managers in asynchronous threads (e.g. when populating the load screen). private readonly ReaderWriterLockSlim ContentManagerLock = new ReaderWriterLockSlim(); + /// A cache of ordered tilesheet IDs used by vanilla maps. + private readonly IDictionary VanillaTilesheets = new Dictionary(StringComparer.OrdinalIgnoreCase); + /// An unmodified content manager which doesn't intercept assets, used to compare asset data. private readonly LocalizedContentManager VanillaContentManager; @@ -293,21 +296,21 @@ namespace StardewModdingAPI.Framework }); } - /// Get a vanilla asset without interception. - /// The type of asset to load. + /// Get the tilesheet ID order used by the unmodified version of a map asset. /// The asset path relative to the loader root directory, not including the .xnb extension. - public bool TryLoadVanillaAsset(string assetName, out T asset) + public TilesheetReference[] GetVanillaTilesheetIds(string assetName) { - try + if (!this.VanillaTilesheets.TryGetValue(assetName, out TilesheetReference[] tilesheets)) { - asset = this.VanillaContentManager.Load(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]; } /// Dispose held resources. @@ -341,5 +344,23 @@ namespace StardewModdingAPI.Framework this.ContentManagers.Remove(contentManager) ); } + + /// Get a vanilla asset without interception. + /// The type of asset to load. + /// The asset path relative to the loader root directory, not including the .xnb extension. + /// The loaded asset data. + private bool TryLoadVanillaAsset(string assetName, out T asset) + { + try + { + asset = this.VanillaContentManager.Load(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..540475f1 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,9 +412,9 @@ 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")}.\n\nTechnical details for mod author:\nExpected order [{string.Join(", ", vanillaTilesheetRefs.Select(p => $"'{p.ImageSource}' (id: {p.Id})"))}], but found tilesheet '{vanillaSheet.Id}' at index {loadedIndex} instead of {vanillaSheet.Index}. Make sure custom tilesheet IDs are prefixed with 'z_' to avoid reordering tilesheets." : $"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); @@ -436,23 +434,15 @@ namespace StardewModdingAPI.Framework.ContentManagers /// Find a map tilesheet by ID. /// The map whose tilesheets to search. /// The tilesheet ID to match. - /// The matched tilesheet index, if any. - /// The matched tilesheet, if any. - 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; } } } -- cgit From a179466e6b2800846bd425e2fe61de80d52d77bd Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 6 Jan 2021 00:44:24 -0500 Subject: remove experimental performance counters Unfortunately this impacted SMAPI's memory usage and the data was often misinterpreted by players. --- src/SMAPI/Constants.cs | 3 - src/SMAPI/Framework/Events/EventManager.cs | 6 +- src/SMAPI/Framework/Events/ManagedEvent.cs | 44 ++--- .../PerformanceMonitoring/AlertContext.cs | 34 ---- .../Framework/PerformanceMonitoring/AlertEntry.cs | 38 ---- .../Framework/PerformanceMonitoring/PeakEntry.cs | 35 ---- .../PerformanceMonitoring/PerformanceCounter.cs | 124 ------------- .../PerformanceCounterCollection.cs | 205 --------------------- .../PerformanceCounterEntry.cs | 30 --- .../PerformanceMonitoring/PerformanceMonitor.cs | 184 ------------------ src/SMAPI/Framework/SCore.cs | 10 +- src/SMAPI/Properties/AssemblyInfo.cs | 1 - 12 files changed, 15 insertions(+), 699 deletions(-) delete mode 100644 src/SMAPI/Framework/PerformanceMonitoring/AlertContext.cs delete mode 100644 src/SMAPI/Framework/PerformanceMonitoring/AlertEntry.cs delete mode 100644 src/SMAPI/Framework/PerformanceMonitoring/PeakEntry.cs delete mode 100644 src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounter.cs delete mode 100644 src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounterCollection.cs delete mode 100644 src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounterEntry.cs delete mode 100644 src/SMAPI/Framework/PerformanceMonitoring/PerformanceMonitor.cs (limited to 'src/SMAPI') diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index ef996c0f..9816013c 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -97,9 +97,6 @@ namespace StardewModdingAPI /// The URL of the SMAPI home page. internal const string HomePageUrl = "https://smapi.io"; - /// The default performance counter name for unknown event handlers. - internal const string GamePerformanceCounterName = ""; - /// The absolute path to the folder containing SMAPI's internal files. internal static readonly string InternalFilesPath = EarlyConstants.InternalFilesPath; 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 *********/ /// Construct an instance. /// The mod registry with which to identify mods. - /// Tracks performance metrics. - public EventManager(ModRegistry modRegistry, PerformanceMonitor performanceMonitor) + public EventManager(ModRegistry modRegistry) { // create shortcut initializers ManagedEvent ManageEventOf(string typeName, string eventName, bool isPerformanceCritical = false) { - return new ManagedEvent($"{typeName}.{eventName}", modRegistry, performanceMonitor, isPerformanceCritical); + return new ManagedEvent($"{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 /// The mod registry with which to identify mods. protected readonly ModRegistry ModRegistry; - /// Tracks performance metrics. - private readonly PerformanceMonitor PerformanceMonitor; - /// The underlying event handlers. private readonly List> Handlers = new List>(); @@ -49,13 +45,11 @@ namespace StardewModdingAPI.Framework.Events /// Construct an instance. /// A human-readable name for the event. /// The mod registry with which to identify mods. - /// Tracks performance metrics. /// Whether the event is typically called at least once per second. - 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 handler in handlers) { - foreach (ManagedEventHandler 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 *********/ - /// Get the mod name for a given event handler to display in performance monitoring reports. - /// The event handler. - private string GetModNameForPerformanceCounters(ManagedEventHandler handler) - { - IModMetadata mod = handler.SourceMod; - - return mod.HasManifest() - ? mod.Manifest.UniqueID - : mod.DisplayName; - } - /// Log an exception from an event handler. /// The event handler instance. /// The exception that was raised. 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 -{ - /// The context for an alert. - internal readonly struct AlertContext - { - /********* - ** Accessors - *********/ - /// The source which triggered the alert. - public string Source { get; } - - /// The elapsed milliseconds. - public double Elapsed { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The source which triggered the alert. - /// The elapsed milliseconds. - public AlertContext(string source, double elapsed) - { - this.Source = source; - this.Elapsed = elapsed; - } - - /// Get a human-readable text form of this instance. - 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 -{ - /// A single alert entry. - internal readonly struct AlertEntry - { - /********* - ** Accessors - *********/ - /// The collection in which the alert occurred. - public PerformanceCounterCollection Collection { get; } - - /// The actual execution time in milliseconds. - public double ExecutionTimeMilliseconds { get; } - - /// The configured alert threshold in milliseconds. - public double ThresholdMilliseconds { get; } - - /// The sources involved in exceeding the threshold. - public AlertContext[] Context { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The collection in which the alert occurred. - /// The actual execution time in milliseconds. - /// The configured alert threshold in milliseconds. - /// The sources involved in exceeding the threshold. - 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 -{ - /// A peak invocation time. - internal readonly struct PeakEntry - { - /********* - ** Accessors - *********/ - /// The actual execution time in milliseconds. - public double ExecutionTimeMilliseconds { get; } - - /// When the entry occurred. - public DateTime EventTime { get; } - - /// The sources involved in exceeding the threshold. - public AlertContext[] Context { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The actual execution time in milliseconds. - /// When the entry occurred. - /// The sources involved in exceeding the threshold. - 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 -{ - /// Tracks metadata about a particular code event. - internal class PerformanceCounter - { - /********* - ** Fields - *********/ - /// The size of the ring buffer. - private readonly int MaxEntries = 16384; - - /// The collection to which this performance counter belongs. - private readonly PerformanceCounterCollection ParentCollection; - - /// The performance counter entries. - private readonly Stack Entries; - - /// The entry with the highest execution time. - private PerformanceCounterEntry? PeakPerformanceCounterEntry; - - - /********* - ** Accessors - *********/ - /// The name of the source. - public string Source { get; } - - /// The alert threshold in milliseconds - public double AlertThresholdMilliseconds { get; set; } - - /// If alerting is enabled or not - public bool EnableAlerts { get; set; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The collection to which this performance counter belongs. - /// The name of the source. - public PerformanceCounter(PerformanceCounterCollection parentCollection, string source) - { - this.ParentCollection = parentCollection; - this.Source = source; - this.Entries = new Stack(this.MaxEntries); - } - - /// Add a performance counter entry to the list, update monitoring, and raise alerts if needed. - /// The entry to add. - 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)); - } - - /// Clear all performance counter entries and monitoring. - public void Reset() - { - this.Entries.Clear(); - this.PeakPerformanceCounterEntry = null; - } - - /// Get the peak entry. - public PerformanceCounterEntry? GetPeak() - { - return this.PeakPerformanceCounterEntry; - } - - /// Get the entry with the highest execution time. - /// The time range to search. - /// The end time for the , or null for the current time. - 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(); - } - - /// Get the last entry added to the list. - public PerformanceCounterEntry? GetLastEntry() - { - if (this.Entries.Count == 0) - return null; - - return this.Entries.Peek(); - } - - /// Get the average over a given time span. - /// The time range to search. - /// The end time for the , or null for the current time. - 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 - *********/ - /// The number of peak invocations to keep. - private readonly int MaxEntries = 16384; - - /// The sources involved in exceeding alert thresholds. - private readonly List TriggeredPerformanceCounters = new List(); - - /// The stopwatch used to track the invocation time. - private readonly Stopwatch InvocationStopwatch = new Stopwatch(); - - /// The performance counter manager. - private readonly PerformanceMonitor PerformanceMonitor; - - /// The time to calculate average calls per second. - private DateTime CallsPerSecondStart = DateTime.UtcNow; - - /// The number of invocations. - private long CallCount; - - /// The peak invocations. - private readonly Stack PeakInvocations; - - - /********* - ** Accessors - *********/ - /// The associated performance counters. - public IDictionary PerformanceCounters { get; } = new Dictionary(); - - /// The name of this collection. - public string Name { get; } - - /// Whether the source is typically invoked at least once per second. - public bool IsPerformanceCritical { get; } - - /// The alert threshold in milliseconds. - public double AlertThresholdMilliseconds { get; set; } - - /// Whether alerts are enabled. - public bool EnableAlerts { get; set; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The performance counter manager. - /// The name of this collection. - /// Whether the source is typically invoked at least once per second. - public PerformanceCounterCollection(PerformanceMonitor performanceMonitor, string name, bool isPerformanceCritical = false) - { - this.PeakInvocations = new Stack(this.MaxEntries); - this.Name = name; - this.PerformanceMonitor = performanceMonitor; - this.IsPerformanceCritical = isPerformanceCritical; - } - - /// Track a single invocation for a named source. - /// The name of the source. - /// The entry. - 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)); - } - - /// Get the average execution time for all non-game internal sources in milliseconds. - /// The interval for which to get the average, relative to now - public double GetModsAverageExecutionTime(TimeSpan interval) - { - return this.PerformanceCounters - .Where(entry => entry.Key != Constants.GamePerformanceCounterName) - .Sum(entry => entry.Value.GetAverage(interval)); - } - - /// Get the overall average execution time in milliseconds. - /// The interval for which to get the average, relative to now - public double GetAverageExecutionTime(TimeSpan interval) - { - return this.PerformanceCounters - .Sum(entry => entry.Value.GetAverage(interval)); - } - - /// Get the average execution time for game-internal sources in milliseconds. - public double GetGameAverageExecutionTime(TimeSpan interval) - { - return this.PerformanceCounters.TryGetValue(Constants.GamePerformanceCounterName, out PerformanceCounter gameExecTime) - ? gameExecTime.GetAverage(interval) - : 0; - } - - /// Get the peak execution time in milliseconds. - /// The time range to search. - /// The end time for the , or null for the current time. - 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(); - } - - /// Start tracking the invocation of this collection. - public void BeginTrackInvocation() - { - this.TriggeredPerformanceCounters.Clear(); - this.InvocationStopwatch.Reset(); - this.InvocationStopwatch.Start(); - - this.CallCount++; - } - - /// End tracking the invocation of this collection, and raise an alert if needed. - 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()); - } - - /// Add an alert. - /// The execution time in milliseconds. - /// The configured threshold. - /// The sources involved in exceeding the threshold. - public void AddAlert(double executionTimeMilliseconds, double thresholdMilliseconds, AlertContext[] alerts) - { - this.PerformanceMonitor.AddAlert( - new AlertEntry(this, executionTimeMilliseconds, thresholdMilliseconds, alerts) - ); - } - - /// Add an alert. - /// The execution time in milliseconds. - /// The configured threshold. - /// The source involved in exceeding the threshold. - public void AddAlert(double executionTimeMilliseconds, double thresholdMilliseconds, AlertContext alert) - { - this.AddAlert(executionTimeMilliseconds, thresholdMilliseconds, new[] { alert }); - } - - /// Reset the calls per second counter. - public void ResetCallsPerSecond() - { - this.CallCount = 0; - this.CallsPerSecondStart = DateTime.UtcNow; - } - - /// Reset all performance counters in this collection. - public void Reset() - { - this.PeakInvocations.Clear(); - foreach (var counter in this.PerformanceCounters) - counter.Value.Reset(); - } - - /// Reset the performance counter for a specific source. - /// The source name. - public void ResetSource(string source) - { - foreach (var i in this.PerformanceCounters) - if (i.Value.Source.Equals(source, StringComparison.OrdinalIgnoreCase)) - i.Value.Reset(); - } - - /// Get the average calls per second. - 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 -{ - /// A single performance counter entry. - internal readonly struct PerformanceCounterEntry - { - /********* - ** Accessors - *********/ - /// When the entry occurred. - public DateTime EventTime { get; } - - /// The elapsed milliseconds. - public double ElapsedMilliseconds { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// When the entry occurred. - /// The elapsed milliseconds. - 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 -{ - /// Tracks performance metrics. - internal class PerformanceMonitor - { - /********* - ** Fields - *********/ - /// The recorded alerts. - private readonly IList Alerts = new List(); - - /// The monitor for output logging. - private readonly IMonitor Monitor; - - /// The invocation stopwatch. - private readonly Stopwatch InvocationStopwatch = new Stopwatch(); - - /// The underlying performance counter collections. - private readonly IDictionary Collections = new Dictionary(StringComparer.OrdinalIgnoreCase); - - - /********* - ** Accessors - *********/ - /// Whether alerts are paused. - public bool PauseAlerts { get; set; } - - /// Whether performance counter tracking is enabled. - public bool EnableTracking { get; set; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The monitor for output logging. - public PerformanceMonitor(IMonitor monitor) - { - this.Monitor = monitor; - } - - /// Reset all performance counters in all collections. - public void Reset() - { - foreach (PerformanceCounterCollection collection in this.Collections.Values) - collection.Reset(); - } - - /// Track the invocation time for a collection. - /// The name of the collection. - /// The action to execute and track. - public void Track(string collectionName, Action action) - { - if (!this.EnableTracking) - { - action(); - return; - } - - PerformanceCounterCollection collection = this.GetOrCreateCollectionByName(collectionName); - collection.BeginTrackInvocation(); - try - { - action(); - } - finally - { - collection.EndTrackInvocation(); - } - } - - /// Track a single performance counter invocation in a specific collection. - /// The name of the collection. - /// The name of the source. - /// The action to execute and track. - 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)); - } - } - - /// Reset the performance counters for a specific collection. - /// The collection name. - public void ResetCollection(string name) - { - if (this.Collections.TryGetValue(name, out PerformanceCounterCollection collection)) - { - collection.ResetCallsPerSecond(); - collection.Reset(); - } - } - - /// Reset performance counters for a specific source. - /// The name of the source. - public void ResetSource(string name) - { - foreach (PerformanceCounterCollection performanceCounterCollection in this.Collections.Values) - performanceCounterCollection.ResetSource(name); - } - - /// Print any queued alerts. - 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); - } - - /// Add an alert to the queue. - /// The alert to add. - public void AddAlert(AlertEntry entry) - { - if (!this.PauseAlerts) - this.Alerts.Add(entry); - } - - /// Initialize the default performance counter collections. - /// The event manager. - public void InitializePerformanceCounterCollections(EventManager eventManager) - { - foreach (IManagedEvent @event in eventManager.GetAllEvents()) - this.Collections[@event.EventName] = new PerformanceCounterCollection(this, @event.EventName, @event.IsPerformanceCritical); - } - - /// Get the underlying performance counters. - public IEnumerable GetCollections() - { - return this.Collections.Values; - } - - - /********* - ** Public methods - *********/ - /// Get a collection by name and creates it if it doesn't exist. - /// The name of the collection. - 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..f9113194 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; @@ -143,10 +142,6 @@ namespace StardewModdingAPI.Framework /// This is initialized after the game starts. This is accessed directly because it's not part of the normal class model. internal static DeprecationManager DeprecationManager { get; private set; } - /// Manages performance counters. - /// This is initialized after the game starts. This is non-private for use by Console Commands. - internal static PerformanceMonitor PerformanceMonitor { get; private set; } - /// The number of update ticks which have already executed. This is similar to , but incremented more consistently for every tick. internal static uint TicksElapsed { get; private set; } @@ -175,9 +170,7 @@ namespace StardewModdingAPI.Framework 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.EventManager = new EventManager(this.ModRegistry); SCore.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry); @@ -443,7 +436,6 @@ namespace StardewModdingAPI.Framework *********/ // print warnings/alerts SCore.DeprecationManager.PrintQueued(); - SCore.PerformanceMonitor.PrintQueuedAlerts(); /********* ** First-tick initialization 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 -- cgit From d5b00bec84fca94ebd94bea509af060928585cc8 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 6 Jan 2021 02:14:44 -0500 Subject: simplify tilesheet order warning --- src/SMAPI/Framework/ContentManagers/GameContentManager.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src/SMAPI') diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index 540475f1..3db3856f 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -414,16 +414,16 @@ namespace StardewModdingAPI.Framework.ContentManagers 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")}.\n\nTechnical details for mod author:\nExpected order [{string.Join(", ", vanillaTilesheetRefs.Select(p => $"'{p.ImageSource}' (id: {p.Id})"))}], but found tilesheet '{vanillaSheet.Id}' at index {loadedIndex} instead of {vanillaSheet.Index}. Make sure custom tilesheet IDs are prefixed with 'z_' to avoid reordering tilesheets." + ? $"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); } } } -- cgit From 51de495ae4c1b9da13cce24dc15ac844b24f657e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 6 Jan 2021 23:43:48 -0500 Subject: add a way to send console commands to a specific screen --- src/SMAPI/Framework/CommandManager.cs | 67 ++++++++++++++++++++++++++++++++++- src/SMAPI/Framework/SCore.cs | 67 +++++++++++++++++++++++------------ src/SMAPI/Utilities/PerScreen.cs | 33 ++++++++++------- 3 files changed, 131 insertions(+), 36 deletions(-) (limited to 'src/SMAPI') 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 /// The commands registered with SMAPI. private readonly IDictionary Commands = new Dictionary(StringComparer.OrdinalIgnoreCase); + /// Writes messages to the console. + private readonly IMonitor Monitor; + /********* ** Public methods *********/ + /// Construct an instance. + /// Writes messages to the console. + public CommandManager(IMonitor monitor) + { + this.Monitor = monitor; + } + /// Add a console command. /// The mod adding the command (or null for a SMAPI command). /// The command name, which the user must type to trigger it. @@ -81,8 +91,9 @@ namespace StardewModdingAPI.Framework /// The parsed command name. /// The parsed command arguments. /// The command which can handle the input. + /// The screen ID on which to run the command. /// Returns true if the input was successfully parsed and matched to a command; else false. - 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(); } + /// Try to parse a 'screen=X' command argument, which specifies the screen that should receive the command. + /// The raw argument to parse. + /// The parsed screen ID, if any. + /// The error which indicates an invalid screen ID, if applicable. + /// Returns whether the screen ID was parsed successfully. + 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; + } + /// Get a normalized command name. /// The command name. private string GetNormalizedName(string name) diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index f9113194..3a51b418 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -81,7 +81,7 @@ namespace StardewModdingAPI.Framework ** Higher-level components ****/ /// Manages console commands. - private readonly CommandManager CommandManager = new CommandManager(); + private readonly CommandManager CommandManager; /// The underlying game instance. private SGameRunner Game; @@ -130,9 +130,12 @@ namespace StardewModdingAPI.Framework /// Asset interceptors added or removed since the last tick. private readonly List ReloadAssetInterceptorsQueue = new List(); - /// A list of queued commands to execute. + /// A list of queued commands to parse and execute. /// This property must be thread-safe, since it's accessed from a separate console input thread. - public ConcurrentQueue CommandQueue { get; } = new ConcurrentQueue(); + private readonly ConcurrentQueue RawCommandQueue = new ConcurrentQueue(); + + /// A list of commands to execute on each screen. + private readonly PerScreen>> ScreenCommandQueue = new(() => new()); /********* @@ -169,11 +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); - + 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 @@ -406,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(); @@ -489,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; @@ -511,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)); } /********* @@ -570,7 +562,9 @@ namespace StardewModdingAPI.Framework try { - // reapply overrides + /********* + ** Reapply overrides + *********/ if (this.JustReturnedToTitle) { if (!(Game1.mapDisplayDevice is SDisplayDevice)) @@ -579,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 *********/ 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 /// The value is initialized the first time it's requested for that player, unless it's set manually first. 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); } + ///