From 8c8ec6a4572fd3e59b738fc7545fcddc764f4519 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 10 May 2022 23:05:24 -0400 Subject: remove unused IsPerformanceCritical event field --- src/SMAPI/Framework/Events/EventManager.cs | 34 ++++++++++++++--------------- src/SMAPI/Framework/Events/IManagedEvent.cs | 3 --- src/SMAPI/Framework/Events/ManagedEvent.cs | 7 +----- 3 files changed, 18 insertions(+), 26 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 41540047..b21d5c7d 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -198,9 +198,9 @@ namespace StardewModdingAPI.Framework.Events public EventManager(ModRegistry modRegistry) { // create shortcut initializers - ManagedEvent ManageEventOf(string typeName, string eventName, bool isPerformanceCritical = false) + ManagedEvent ManageEventOf(string typeName, string eventName) { - return new ManagedEvent($"{typeName}.{eventName}", modRegistry, isPerformanceCritical); + return new ManagedEvent($"{typeName}.{eventName}", modRegistry); } // init events @@ -210,21 +210,21 @@ namespace StardewModdingAPI.Framework.Events this.LocaleChanged = ManageEventOf(nameof(IModEvents.Content), nameof(IContentEvents.LocaleChanged)); this.MenuChanged = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.MenuChanged)); - this.Rendering = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.Rendering), isPerformanceCritical: true); - this.Rendered = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.Rendered), isPerformanceCritical: true); - this.RenderingWorld = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingWorld), isPerformanceCritical: true); - this.RenderedWorld = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedWorld), isPerformanceCritical: true); - this.RenderingActiveMenu = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingActiveMenu), isPerformanceCritical: true); - this.RenderedActiveMenu = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedActiveMenu), isPerformanceCritical: true); - this.RenderingHud = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingHud), isPerformanceCritical: true); - this.RenderedHud = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedHud), isPerformanceCritical: true); + this.Rendering = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.Rendering)); + this.Rendered = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.Rendered)); + this.RenderingWorld = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingWorld)); + this.RenderedWorld = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedWorld)); + this.RenderingActiveMenu = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingActiveMenu)); + this.RenderedActiveMenu = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedActiveMenu)); + this.RenderingHud = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingHud)); + this.RenderedHud = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedHud)); this.WindowResized = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.WindowResized)); this.GameLaunched = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.GameLaunched)); - this.UpdateTicking = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.UpdateTicking), isPerformanceCritical: true); - this.UpdateTicked = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.UpdateTicked), isPerformanceCritical: true); - this.OneSecondUpdateTicking = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.OneSecondUpdateTicking), isPerformanceCritical: true); - this.OneSecondUpdateTicked = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.OneSecondUpdateTicked), isPerformanceCritical: true); + this.UpdateTicking = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.UpdateTicking)); + this.UpdateTicked = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.UpdateTicked)); + this.OneSecondUpdateTicking = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.OneSecondUpdateTicking)); + this.OneSecondUpdateTicked = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.OneSecondUpdateTicked)); this.SaveCreating = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.SaveCreating)); this.SaveCreated = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.SaveCreated)); this.Saving = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.Saving)); @@ -238,7 +238,7 @@ namespace StardewModdingAPI.Framework.Events this.ButtonsChanged = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonsChanged)); this.ButtonPressed = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonPressed)); this.ButtonReleased = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonReleased)); - this.CursorMoved = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.CursorMoved), isPerformanceCritical: true); + this.CursorMoved = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.CursorMoved)); this.MouseWheelScrolled = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.MouseWheelScrolled)); this.PeerContextReceived = ManageEventOf(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.PeerContextReceived)); @@ -261,8 +261,8 @@ namespace StardewModdingAPI.Framework.Events this.FurnitureListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.FurnitureListChanged)); this.LoadStageChanged = ManageEventOf(nameof(IModEvents.Specialized), nameof(ISpecializedEvents.LoadStageChanged)); - this.UnvalidatedUpdateTicking = ManageEventOf(nameof(IModEvents.Specialized), nameof(ISpecializedEvents.UnvalidatedUpdateTicking), isPerformanceCritical: true); - this.UnvalidatedUpdateTicked = ManageEventOf(nameof(IModEvents.Specialized), nameof(ISpecializedEvents.UnvalidatedUpdateTicked), isPerformanceCritical: true); + this.UnvalidatedUpdateTicking = ManageEventOf(nameof(IModEvents.Specialized), nameof(ISpecializedEvents.UnvalidatedUpdateTicking)); + this.UnvalidatedUpdateTicked = ManageEventOf(nameof(IModEvents.Specialized), nameof(ISpecializedEvents.UnvalidatedUpdateTicked)); } } } diff --git a/src/SMAPI/Framework/Events/IManagedEvent.cs b/src/SMAPI/Framework/Events/IManagedEvent.cs index e4e3ca08..0ae8c55a 100644 --- a/src/SMAPI/Framework/Events/IManagedEvent.cs +++ b/src/SMAPI/Framework/Events/IManagedEvent.cs @@ -8,8 +8,5 @@ namespace StardewModdingAPI.Framework.Events *********/ /// A human-readable name for the event. string EventName { get; } - - /// Whether the event is typically called at least once per second. - bool IsPerformanceCritical { get; } } } diff --git a/src/SMAPI/Framework/Events/ManagedEvent.cs b/src/SMAPI/Framework/Events/ManagedEvent.cs index 4b8a770d..a16beb77 100644 --- a/src/SMAPI/Framework/Events/ManagedEvent.cs +++ b/src/SMAPI/Framework/Events/ManagedEvent.cs @@ -36,9 +36,6 @@ namespace StardewModdingAPI.Framework.Events /// public string EventName { get; } - /// - public bool IsPerformanceCritical { get; } - /********* ** Public methods @@ -46,12 +43,10 @@ namespace StardewModdingAPI.Framework.Events /// Construct an instance. /// A human-readable name for the event. /// The mod registry with which to identify mods. - /// Whether the event is typically called at least once per second. - public ManagedEvent(string eventName, ModRegistry modRegistry, bool isPerformanceCritical = false) + public ManagedEvent(string eventName, ModRegistry modRegistry) { this.EventName = eventName; this.ModRegistry = modRegistry; - this.IsPerformanceCritical = isPerformanceCritical; } /// Get whether anything is listening to the event. -- cgit From e14916f9622592a993fad54fe184a03de0b95c7b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 11 May 2022 17:12:58 -0400 Subject: add error code to SContentLoadException --- .../ContentManagers/BaseContentManager.cs | 4 ++-- .../Framework/ContentManagers/ModContentManager.cs | 27 +++++++++++----------- .../Framework/Exceptions/ContentLoadErrorType.cs | 21 +++++++++++++++++ .../Framework/Exceptions/SContentLoadException.cs | 15 ++++++++++-- src/SMAPI/Framework/ModHelpers/ContentHelper.cs | 4 ++-- .../Framework/ModHelpers/GameContentHelper.cs | 2 +- src/SMAPI/Framework/ModHelpers/ModContentHelper.cs | 2 +- 7 files changed, 54 insertions(+), 21 deletions(-) create mode 100644 src/SMAPI/Framework/Exceptions/ContentLoadErrorType.cs (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs index e4695588..575d252e 100644 --- a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs @@ -198,9 +198,9 @@ namespace StardewModdingAPI.Framework.ContentManagers // NOTE: the game checks for ContentLoadException to handle invalid keys, so avoid // throwing other types like ArgumentException here. if (string.IsNullOrWhiteSpace(assetName)) - throw new SContentLoadException("The asset key or local path is empty."); + throw new SContentLoadException(ContentLoadErrorType.InvalidName, "The asset key or local path is empty."); if (assetName.Intersect(Path.GetInvalidPathChars()).Any()) - throw new SContentLoadException("The asset key or local path contains invalid characters."); + throw new SContentLoadException(ContentLoadErrorType.InvalidName, "The asset key or local path contains invalid characters."); return this.Cache.NormalizeKey(assetName); } diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index 7cac8f36..85e109c8 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -94,7 +94,7 @@ namespace StardewModdingAPI.Framework.ContentManagers if (this.Coordinator.TryParseManagedAssetKey(assetName.Name, out string? contentManagerID, out IAssetName? relativePath)) { if (contentManagerID != this.Name) - throw this.GetLoadError(assetName, "can't load a different mod's managed asset key through this mod content manager."); + throw this.GetLoadError(assetName, ContentLoadErrorType.AccessDenied, "can't load a different mod's managed asset key through this mod content manager."); assetName = relativePath; } } @@ -106,7 +106,7 @@ namespace StardewModdingAPI.Framework.ContentManagers // get file FileInfo file = this.GetModFile(assetName.Name); if (!file.Exists) - throw this.GetLoadError(assetName, "the specified path doesn't exist."); + throw this.GetLoadError(assetName, ContentLoadErrorType.AssetDoesNotExist, "the specified path doesn't exist."); // load content asset = file.Extension.ToLower() switch @@ -121,7 +121,7 @@ namespace StardewModdingAPI.Framework.ContentManagers } catch (Exception ex) when (ex is not SContentLoadException) { - throw this.GetLoadError(assetName, "an unexpected occurred.", ex); + throw this.GetLoadError(assetName, ContentLoadErrorType.Other, "an unexpected occurred.", ex); } // track & return asset @@ -157,7 +157,7 @@ namespace StardewModdingAPI.Framework.ContentManagers { // validate if (!typeof(T).IsAssignableFrom(typeof(XmlSource))) - throw this.GetLoadError(assetName, $"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(XmlSource)}'."); + throw this.GetLoadError(assetName, ContentLoadErrorType.InvalidData, $"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(XmlSource)}'."); // load string source = File.ReadAllText(file.FullName); @@ -171,7 +171,7 @@ namespace StardewModdingAPI.Framework.ContentManagers private T LoadDataFile(IAssetName assetName, FileInfo file) { if (!this.JsonHelper.ReadJsonFileIfExists(file.FullName, out T? asset)) - throw this.GetLoadError(assetName, "the JSON file is invalid."); // should never happen since we check for file existence before calling this method + throw this.GetLoadError(assetName, ContentLoadErrorType.InvalidData, "the JSON file is invalid."); // should never happen since we check for file existence before calling this method return asset; } @@ -184,7 +184,7 @@ namespace StardewModdingAPI.Framework.ContentManagers { // validate if (typeof(T) != typeof(Texture2D)) - throw this.GetLoadError(assetName, $"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Texture2D)}'."); + throw this.GetLoadError(assetName, ContentLoadErrorType.InvalidData, $"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Texture2D)}'."); // load using FileStream stream = File.OpenRead(file.FullName); @@ -201,7 +201,7 @@ namespace StardewModdingAPI.Framework.ContentManagers { // validate if (typeof(T) != typeof(Map)) - throw this.GetLoadError(assetName, $"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Map)}'."); + throw this.GetLoadError(assetName, ContentLoadErrorType.InvalidData, $"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Map)}'."); // load FormatManager formatManager = FormatManager.Instance; @@ -239,16 +239,17 @@ namespace StardewModdingAPI.Framework.ContentManagers /// The file to load. private T HandleUnknownFileType(IAssetName assetName, FileInfo file) { - throw this.GetLoadError(assetName, $"unknown file extension '{file.Extension}'; must be one of '.fnt', '.json', '.png', '.tbin', '.tmx', or '.xnb'."); + throw this.GetLoadError(assetName, ContentLoadErrorType.InvalidName, $"unknown file extension '{file.Extension}'; must be one of '.fnt', '.json', '.png', '.tbin', '.tmx', or '.xnb'."); } /// Get an error which indicates that an asset couldn't be loaded. + /// Why loading an asset through the content pipeline failed. /// The asset name that failed to load. /// The reason the file couldn't be loaded. /// The underlying exception, if applicable. - private SContentLoadException GetLoadError(IAssetName assetName, string reasonPhrase, Exception? exception = null) + private SContentLoadException GetLoadError(IAssetName assetName, ContentLoadErrorType errorType, string reasonPhrase, Exception? exception = null) { - return new($"Failed loading asset '{assetName}' from {this.Name}: {reasonPhrase}", exception); + return new(errorType, $"Failed loading asset '{assetName}' from {this.Name}: {reasonPhrase}", exception); } /// Get a file from the mod folder. @@ -328,13 +329,13 @@ namespace StardewModdingAPI.Framework.ContentManagers // validate tilesheet path string errorPrefix = $"{this.ModName} loaded map '{relativeMapPath}' with invalid tilesheet path '{imageSource}'."; if (Path.IsPathRooted(imageSource) || PathUtilities.GetSegments(imageSource).Contains("..")) - throw new SContentLoadException($"{errorPrefix} Tilesheet paths must be a relative path without directory climbing (../)."); + throw new SContentLoadException(ContentLoadErrorType.InvalidData, $"{errorPrefix} Tilesheet paths must be a relative path without directory climbing (../)."); // load best match try { if (!this.TryGetTilesheetAssetName(relativeMapFolder, imageSource, out IAssetName? assetName, out string? error)) - throw new SContentLoadException($"{errorPrefix} {error}"); + throw new SContentLoadException(ContentLoadErrorType.InvalidData, $"{errorPrefix} {error}"); if (assetName is not null) { @@ -346,7 +347,7 @@ namespace StardewModdingAPI.Framework.ContentManagers } catch (Exception ex) when (ex is not SContentLoadException) { - throw new SContentLoadException($"{errorPrefix} The tilesheet couldn't be loaded.", ex); + throw new SContentLoadException(ContentLoadErrorType.InvalidData, $"{errorPrefix} The tilesheet couldn't be loaded.", ex); } } } diff --git a/src/SMAPI/Framework/Exceptions/ContentLoadErrorType.cs b/src/SMAPI/Framework/Exceptions/ContentLoadErrorType.cs new file mode 100644 index 00000000..16689b67 --- /dev/null +++ b/src/SMAPI/Framework/Exceptions/ContentLoadErrorType.cs @@ -0,0 +1,21 @@ +namespace StardewModdingAPI.Framework.Exceptions +{ + /// Indicates why loading an asset through the content pipeline failed. + internal enum ContentLoadErrorType + { + /// The asset name is empty or has an invalid format. + InvalidName, + + /// The asset doesn't exist. + AssetDoesNotExist, + + /// The asset is not available in the current context (e.g. an attempt to load another mod's assets). + AccessDenied, + + /// The asset exists, but the data could not be deserialized or it doesn't match the expected type. + InvalidData, + + /// An unknown error occurred. + Other + } +} diff --git a/src/SMAPI/Framework/Exceptions/SContentLoadException.cs b/src/SMAPI/Framework/Exceptions/SContentLoadException.cs index be1fe748..4db24d06 100644 --- a/src/SMAPI/Framework/Exceptions/SContentLoadException.cs +++ b/src/SMAPI/Framework/Exceptions/SContentLoadException.cs @@ -6,13 +6,24 @@ namespace StardewModdingAPI.Framework.Exceptions /// An implementation of used by SMAPI to detect whether it was thrown by SMAPI or the underlying framework. internal class SContentLoadException : ContentLoadException { + /********* + ** Accessors + *********/ + /// Why loading the asset through the content pipeline failed. + public ContentLoadErrorType ErrorType { get; } + + /********* ** Public methods *********/ /// Construct an instance. + /// Why loading the asset through the content pipeline failed. /// The error message. /// The underlying exception, if any. - public SContentLoadException(string message, Exception? ex = null) - : base(message, ex) { } + public SContentLoadException(ContentLoadErrorType errorType, string message, Exception? ex = null) + : base(message, ex) + { + this.ErrorType = errorType; + } } } diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs index 6a92da24..24e511c3 100644 --- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs @@ -135,12 +135,12 @@ namespace StardewModdingAPI.Framework.ModHelpers return this.ModContentManager.LoadExact(assetName, useCache: false); default: - throw new SContentLoadException($"{this.Mod.DisplayName} failed loading content asset '{key}' from {source}: unknown content source '{source}'."); + throw new SContentLoadException(ContentLoadErrorType.Other, $"{this.Mod.DisplayName} failed loading content asset '{key}' from {source}: unknown content source '{source}'."); } } catch (Exception ex) when (ex is not SContentLoadException) { - throw new SContentLoadException($"{this.Mod.DisplayName} failed loading content asset '{key}' from {source}.", ex); + throw new SContentLoadException(ContentLoadErrorType.Other, $"{this.Mod.DisplayName} failed loading content asset '{key}' from {source}.", ex); } } diff --git a/src/SMAPI/Framework/ModHelpers/GameContentHelper.cs b/src/SMAPI/Framework/ModHelpers/GameContentHelper.cs index 232e9287..7c4eda89 100644 --- a/src/SMAPI/Framework/ModHelpers/GameContentHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/GameContentHelper.cs @@ -85,7 +85,7 @@ namespace StardewModdingAPI.Framework.ModHelpers } catch (Exception ex) when (ex is not SContentLoadException) { - throw new SContentLoadException($"{this.ModName} failed loading content asset '{assetName}' from the game content.", ex); + throw new SContentLoadException(ContentLoadErrorType.Other, $"{this.ModName} failed loading content asset '{assetName}' from the game content.", ex); } } diff --git a/src/SMAPI/Framework/ModHelpers/ModContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ModContentHelper.cs index 6429f9bf..5fcb80b2 100644 --- a/src/SMAPI/Framework/ModHelpers/ModContentHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModContentHelper.cs @@ -58,7 +58,7 @@ namespace StardewModdingAPI.Framework.ModHelpers } catch (Exception ex) when (ex is not SContentLoadException) { - throw new SContentLoadException($"{this.ModName} failed loading content asset '{relativePath}' from its mod folder.", ex); + throw new SContentLoadException(ContentLoadErrorType.Other, $"{this.ModName} failed loading content asset '{relativePath}' from its mod folder.", ex); } } -- cgit From d097825c84bbe7d4b4812d4948358dd22abd166a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 11 May 2022 17:25:06 -0400 Subject: fix error when mod loads XNB mod file without extension --- src/SMAPI/Framework/ModHelpers/ContentHelper.cs | 27 ++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs index 24e511c3..427adac2 100644 --- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs @@ -132,7 +132,32 @@ namespace StardewModdingAPI.Framework.ModHelpers return this.GameContentManager.LoadLocalized(assetName, this.CurrentLocaleConstant, useCache: false); case ContentSource.ModFolder: - return this.ModContentManager.LoadExact(assetName, useCache: false); + try + { + return this.ModContentManager.LoadExact(assetName, useCache: false); + } + catch (SContentLoadException ex) when (ex.ErrorType == ContentLoadErrorType.AssetDoesNotExist) + { + // legacy behavior: you can load a .xnb file without the file extension + try + { + IAssetName newName = this.ContentCore.ParseAssetName(assetName.Name + ".xnb", allowLocales: false); + if (this.ModContentManager.DoesAssetExist(newName)) + { + T data = this.ModContentManager.LoadExact(newName, useCache: false); + SCore.DeprecationManager.Warn( + this.Mod, + "loading XNB files from the mod folder without the .xnb file extension", + "3.14.0", + DeprecationLevel.Notice + ); + return data; + } + } + catch { /* legacy behavior failed, rethrow original error */ } + + throw; + } default: throw new SContentLoadException(ContentLoadErrorType.Other, $"{this.Mod.DisplayName} failed loading content asset '{key}' from {source}: unknown content source '{source}'."); -- cgit From 45f674303454fb27327a0404ed403ac15ed04580 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 11 May 2022 17:59:44 -0400 Subject: optimize raising events for the most common cases --- src/SMAPI/Framework/Events/ManagedEvent.cs | 81 ++++++++++++++++++++---------- src/SMAPI/Framework/SCore.cs | 3 +- 2 files changed, 57 insertions(+), 27 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/Events/ManagedEvent.cs b/src/SMAPI/Framework/Events/ManagedEvent.cs index a16beb77..abeea098 100644 --- a/src/SMAPI/Framework/Events/ManagedEvent.cs +++ b/src/SMAPI/Framework/Events/ManagedEvent.cs @@ -20,14 +20,17 @@ namespace StardewModdingAPI.Framework.Events /// The underlying event handlers. private readonly List> Handlers = new(); - /// A cached snapshot of , or null to rebuild it next raise. + /// A cached snapshot of the sorted by event priority, or null to rebuild it next raise. private ManagedEventHandler[]? CachedHandlers = Array.Empty>(); /// The total number of event handlers registered for this events, regardless of whether they're still registered. private int RegistrationIndex; - /// Whether new handlers were added since the last raise. - private bool HasNewHandlers; + /// Whether handlers were removed since the last raise. + private bool HasRemovedHandlers; + + /// Whether any of the handlers have a custom priority. + private bool HasPriorities; /********* @@ -67,7 +70,7 @@ namespace StardewModdingAPI.Framework.Events this.Handlers.Add(managedHandler); this.CachedHandlers = null; - this.HasNewHandlers = true; + this.HasPriorities |= priority != EventPriority.Normal; } } @@ -85,6 +88,7 @@ namespace StardewModdingAPI.Framework.Events this.Handlers.RemoveAt(i); this.CachedHandlers = null; + this.HasRemovedHandlers = true; break; } } @@ -92,10 +96,24 @@ namespace StardewModdingAPI.Framework.Events /// Raise the event and notify all handlers. /// The event arguments to pass. - /// A lambda which returns true if the event should be raised for the given mod. - public void Raise(TEventArgs args, Func? match = null) + public void Raise(TEventArgs args) { - this.Raise((_, invoke) => invoke(args), match); + // skip if no handlers + if (this.Handlers.Count == 0) + return; + + // raise event + foreach (ManagedEventHandler handler in this.GetHandlers()) + { + try + { + handler.Handler(null, args); + } + catch (Exception ex) + { + this.LogError(handler, ex); + } + } } /// Raise the event and notify all handlers. @@ -107,31 +125,15 @@ namespace StardewModdingAPI.Framework.Events if (this.Handlers.Count == 0) return; - // update cached data - // (This is debounced here to avoid repeatedly sorting when handlers are added/removed, - // and keeping a separate cached list allows changes during enumeration.) - var handlers = this.CachedHandlers; // iterate local copy in case a mod adds/removes a handler while handling the event, which will set this field to null - if (handlers == null) - { - lock (this.Handlers) - { - if (this.HasNewHandlers && this.Handlers.Any(p => p.Priority != EventPriority.Normal)) - this.Handlers.Sort(); - - this.CachedHandlers = handlers = this.Handlers.ToArray(); - this.HasNewHandlers = false; - } - } - // raise event - foreach (ManagedEventHandler handler in handlers) + foreach (ManagedEventHandler handler in this.GetHandlers()) { if (match != null && !match(handler.SourceMod)) continue; try { - invoke(handler.SourceMod, args => handler.Handler.Invoke(null, args)); + invoke(handler.SourceMod, args => handler.Handler(null, args)); } catch (Exception ex) { @@ -147,9 +149,36 @@ namespace StardewModdingAPI.Framework.Events /// Log an exception from an event handler. /// The event handler instance. /// The exception that was raised. - protected void LogError(ManagedEventHandler handler, Exception ex) + private void LogError(ManagedEventHandler handler, Exception ex) { handler.SourceMod.LogAsMod($"This mod failed in the {this.EventName} event. Technical details: \n{ex.GetLogSummary()}", LogLevel.Error); } + + /// Get cached copy of the sorted handlers to invoke. + /// This returns the handlers sorted by priority, and allows iterating the list even if a mod adds/removes handlers while handling it. This is debounced when requested to avoid repeatedly sorting when handlers are added/removed. + private ManagedEventHandler[] GetHandlers() + { + ManagedEventHandler[]? handlers = this.CachedHandlers; + + if (handlers == null) + { + lock (this.Handlers) + { + // recheck priorities + if (this.HasRemovedHandlers) + this.HasPriorities = this.Handlers.Any(p => p.Priority != EventPriority.Normal); + + // sort by priority if needed + if (this.HasPriorities) + this.Handlers.Sort(); + + // update cache + this.CachedHandlers = handlers = this.Handlers.ToArray(); + this.HasRemovedHandlers = false; + } + } + + return handlers; + } } } diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index f882682e..c3f0c05f 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -1231,7 +1231,8 @@ namespace StardewModdingAPI.Framework modIDs.Remove(message.FromModID); // don't send a broadcast back to the sender // raise events - this.EventManager.ModMessageReceived.Raise(new ModMessageReceivedEventArgs(message, this.Toolkit.JsonHelper), mod => modIDs.Contains(mod.Manifest.UniqueID)); + var args = new ModMessageReceivedEventArgs(message, this.Toolkit.JsonHelper); + this.EventManager.ModMessageReceived.Raise((_, invoke) => invoke(args), mod => modIDs.Contains(mod.Manifest.UniqueID)); } /// Constructor a content manager to read game content files. -- cgit From cae1063ad99a29aed3a0c162bad1d2842376c608 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 11 May 2022 19:15:22 -0400 Subject: move filtering only used in one place out of managed event --- src/SMAPI/Framework/Events/ManagedEvent.cs | 6 +----- src/SMAPI/Framework/SCore.cs | 13 +++++++++++-- 2 files changed, 12 insertions(+), 7 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/Events/ManagedEvent.cs b/src/SMAPI/Framework/Events/ManagedEvent.cs index abeea098..a34106ce 100644 --- a/src/SMAPI/Framework/Events/ManagedEvent.cs +++ b/src/SMAPI/Framework/Events/ManagedEvent.cs @@ -118,8 +118,7 @@ namespace StardewModdingAPI.Framework.Events /// Raise the event and notify all handlers. /// Invoke an event handler. This receives the mod which registered the handler, and should invoke the callback with the event arguments to pass it. - /// A lambda which returns true if the event should be raised for the given mod. - public void Raise(Action> invoke, Func? match = null) + public void Raise(Action> invoke) { // skip if no handlers if (this.Handlers.Count == 0) @@ -128,9 +127,6 @@ namespace StardewModdingAPI.Framework.Events // raise event foreach (ManagedEventHandler handler in this.GetHandlers()) { - if (match != null && !match(handler.SourceMod)) - continue; - try { invoke(handler.SourceMod, args => handler.Handler(null, args)); diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index c3f0c05f..1fea6d69 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -1231,8 +1231,17 @@ namespace StardewModdingAPI.Framework modIDs.Remove(message.FromModID); // don't send a broadcast back to the sender // raise events - var args = new ModMessageReceivedEventArgs(message, this.Toolkit.JsonHelper); - this.EventManager.ModMessageReceived.Raise((_, invoke) => invoke(args), mod => modIDs.Contains(mod.Manifest.UniqueID)); + ModMessageReceivedEventArgs? args = null; + this.EventManager.ModMessageReceived.Raise( + invoke: (mod, invoke) => + { + if (modIDs.Contains(mod.Manifest.UniqueID)) + { + args ??= new(message, this.Toolkit.JsonHelper); + invoke(args); + } + } + ); } /// Constructor a content manager to read game content files. -- cgit From 05b39b7cd9a40f1660c2c53ef70b75a67f2fccc3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 11 May 2022 19:26:28 -0400 Subject: cache verbose flag in main update method --- src/SMAPI/Framework/SCore.cs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 1fea6d69..667fcf0f 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -577,6 +577,7 @@ namespace StardewModdingAPI.Framework private void OnPlayerInstanceUpdating(SGame instance, GameTime gameTime, Action runUpdate) { EventManager events = this.EventManager; + bool verbose = this.Monitor.IsVerbose; try { @@ -804,7 +805,7 @@ namespace StardewModdingAPI.Framework // since the game adds & removes its own handler on the fly. if (state.WindowSize.IsChanged) { - if (this.Monitor.IsVerbose) + if (verbose) this.Monitor.Log($"Events: window size changed to {state.WindowSize.New}."); events.WindowResized.Raise(new WindowResizedEventArgs(state.WindowSize.Old, state.WindowSize.New)); @@ -828,7 +829,7 @@ namespace StardewModdingAPI.Framework // raise mouse wheel scrolled if (state.MouseWheelScroll.IsChanged) { - if (this.Monitor.IsVerbose) + if (verbose) this.Monitor.Log($"Events: mouse wheel scrolled to {state.MouseWheelScroll.New}."); events.MouseWheelScrolled.Raise(new MouseWheelScrolledEventArgs(cursor, state.MouseWheelScroll.Old, state.MouseWheelScroll.New)); } @@ -845,14 +846,14 @@ namespace StardewModdingAPI.Framework if (status == SButtonState.Pressed) { - if (this.Monitor.IsVerbose) + if (verbose) this.Monitor.Log($"Events: button {button} pressed."); events.ButtonPressed.Raise(new ButtonPressedEventArgs(button, cursor, inputState)); } else if (status == SButtonState.Released) { - if (this.Monitor.IsVerbose) + if (verbose) this.Monitor.Log($"Events: button {button} released."); events.ButtonReleased.Raise(new ButtonReleasedEventArgs(button, cursor, inputState)); @@ -870,7 +871,7 @@ namespace StardewModdingAPI.Framework var was = state.ActiveMenu.Old; var now = state.ActiveMenu.New; - if (this.Monitor.IsVerbose) + if (verbose) this.Monitor.Log($"Context: menu changed from {was?.GetType().FullName ?? "none"} to {now?.GetType().FullName ?? "none"}."); // raise menu events @@ -885,12 +886,12 @@ namespace StardewModdingAPI.Framework bool raiseWorldEvents = !state.SaveID.IsChanged; // don't report changes from unloaded => loaded // location list changes - if (state.Locations.LocationList.IsChanged && (events.LocationListChanged.HasListeners() || this.Monitor.IsVerbose)) + if (state.Locations.LocationList.IsChanged && (events.LocationListChanged.HasListeners() || verbose)) { var added = state.Locations.LocationList.Added.ToArray(); var removed = state.Locations.LocationList.Removed.ToArray(); - if (this.Monitor.IsVerbose) + if (verbose) { string addedText = added.Any() ? string.Join(", ", added.Select(p => p.Name)) : "none"; string removedText = removed.Any() ? string.Join(", ", removed.Select(p => p.Name)) : "none"; @@ -960,7 +961,7 @@ namespace StardewModdingAPI.Framework // raise current location changed if (playerState.Location.IsChanged) { - if (this.Monitor.IsVerbose) + if (verbose) this.Monitor.Log($"Context: set location to {playerState.Location.New}."); events.Warped.Raise(new WarpedEventArgs(player, playerState.Location.Old!, playerState.Location.New!)); @@ -972,7 +973,7 @@ namespace StardewModdingAPI.Framework if (!value.IsChanged) continue; - if (this.Monitor.IsVerbose) + if (verbose) this.Monitor.Log($"Events: player skill '{skill}' changed from {value.Old} to {value.New}."); events.LevelChanged.Raise(new LevelChangedEventArgs(player, skill, value.Old, value.New)); @@ -983,7 +984,7 @@ namespace StardewModdingAPI.Framework { SnapshotItemListDiff inventory = playerState.Inventory; - if (this.Monitor.IsVerbose) + if (verbose) this.Monitor.Log("Events: player inventory changed."); events.InventoryChanged.Raise(new InventoryChangedEventArgs(player, added: inventory.Added, removed: inventory.Removed, quantityChanged: inventory.QuantityChanged)); } -- cgit From 077c897d53371d12c812be5145d917c59583d7cb Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 11 May 2022 19:29:57 -0400 Subject: replace event.HasListeners() with property --- src/SMAPI/Framework/Events/IManagedEvent.cs | 3 +++ src/SMAPI/Framework/Events/ManagedEvent.cs | 11 +++++------ src/SMAPI/Framework/SCore.cs | 10 +++++----- src/SMAPI/Framework/SGame.cs | 4 ++-- 4 files changed, 15 insertions(+), 13 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/Events/IManagedEvent.cs b/src/SMAPI/Framework/Events/IManagedEvent.cs index 0ae8c55a..55994c04 100644 --- a/src/SMAPI/Framework/Events/IManagedEvent.cs +++ b/src/SMAPI/Framework/Events/IManagedEvent.cs @@ -8,5 +8,8 @@ namespace StardewModdingAPI.Framework.Events *********/ /// A human-readable name for the event. string EventName { get; } + + /// Whether any handlers are listening to the event. + bool HasListeners { get; } } } diff --git a/src/SMAPI/Framework/Events/ManagedEvent.cs b/src/SMAPI/Framework/Events/ManagedEvent.cs index a34106ce..8a3ca839 100644 --- a/src/SMAPI/Framework/Events/ManagedEvent.cs +++ b/src/SMAPI/Framework/Events/ManagedEvent.cs @@ -39,6 +39,9 @@ namespace StardewModdingAPI.Framework.Events /// public string EventName { get; } + /// + public bool HasListeners { get; private set; } + /********* ** Public methods @@ -52,12 +55,6 @@ namespace StardewModdingAPI.Framework.Events this.ModRegistry = modRegistry; } - /// Get whether anything is listening to the event. - public bool HasListeners() - { - return this.Handlers.Count > 0; - } - /// Add an event handler. /// The event handler. /// The mod which added the event handler. @@ -70,6 +67,7 @@ namespace StardewModdingAPI.Framework.Events this.Handlers.Add(managedHandler); this.CachedHandlers = null; + this.HasListeners = true; this.HasPriorities |= priority != EventPriority.Normal; } } @@ -88,6 +86,7 @@ namespace StardewModdingAPI.Framework.Events this.Handlers.RemoveAt(i); this.CachedHandlers = null; + this.HasListeners = this.Handlers.Count != 0; this.HasRemovedHandlers = true; break; } diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 667fcf0f..c1e03634 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -886,7 +886,7 @@ namespace StardewModdingAPI.Framework bool raiseWorldEvents = !state.SaveID.IsChanged; // don't report changes from unloaded => loaded // location list changes - if (state.Locations.LocationList.IsChanged && (events.LocationListChanged.HasListeners() || verbose)) + if (state.Locations.LocationList.IsChanged && (events.LocationListChanged.HasListeners || verbose)) { var added = state.Locations.LocationList.Added.ToArray(); var removed = state.Locations.LocationList.Removed.ToArray(); @@ -929,7 +929,7 @@ namespace StardewModdingAPI.Framework events.ObjectListChanged.Raise(new ObjectListChangedEventArgs(location, locState.Objects.Added, locState.Objects.Removed)); // chest items changed - if (events.ChestInventoryChanged.HasListeners()) + if (events.ChestInventoryChanged.HasListeners) { foreach (var pair in locState.ChestItems) { @@ -1077,7 +1077,7 @@ namespace StardewModdingAPI.Framework } // raise event - if (this.EventManager.LocaleChanged.HasListeners()) + if (this.EventManager.LocaleChanged.HasListeners) { this.EventManager.LocaleChanged.Raise( new LocaleChangedEventArgs( @@ -1140,7 +1140,7 @@ namespace StardewModdingAPI.Framework /// The asset name that was loaded. private void OnAssetLoaded(IContentManager contentManager, IAssetName assetName) { - if (this.EventManager.AssetReady.HasListeners()) + if (this.EventManager.AssetReady.HasListeners) this.EventManager.AssetReady.Raise(new AssetReadyEventArgs(assetName, assetName.GetBaseAssetName())); } @@ -1148,7 +1148,7 @@ namespace StardewModdingAPI.Framework /// The invalidated asset names. private void OnAssetsInvalidated(IList assetNames) { - if (this.EventManager.AssetsInvalidated.HasListeners()) + if (this.EventManager.AssetsInvalidated.HasListeners) this.EventManager.AssetsInvalidated.Raise(new AssetsInvalidatedEventArgs(assetNames, assetNames.Select(p => p.GetBaseAssetName()))); } diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 0a8a068f..38043e1c 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -353,7 +353,7 @@ namespace StardewModdingAPI.Framework } if (Game1.currentMinigame != null) { - if (events.Rendering.HasListeners()) + if (events.Rendering.HasListeners) { Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); events.Rendering.RaiseEmpty(); @@ -372,7 +372,7 @@ namespace StardewModdingAPI.Framework Game1.PushUIMode(); this.drawOverlays(Game1.spriteBatch); Game1.PopUIMode(); - if (events.Rendered.HasListeners()) + if (events.Rendered.HasListeners) { Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); events.Rendered.RaiseEmpty(); -- cgit From 42a797a01240893e9a8e645253a269087b2d178d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 11 May 2022 19:56:45 -0400 Subject: don't raise events that have no listeners This mainly avoids allocating event arg objects unnecessarily. --- src/SMAPI/Framework/InternalExtensions.cs | 3 +- src/SMAPI/Framework/SCore.cs | 175 ++++++++++++++++++------------ src/SMAPI/Framework/SMultiplayer.cs | 12 +- 3 files changed, 115 insertions(+), 75 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/InternalExtensions.cs b/src/SMAPI/Framework/InternalExtensions.cs index 580651f3..ba9bbcec 100644 --- a/src/SMAPI/Framework/InternalExtensions.cs +++ b/src/SMAPI/Framework/InternalExtensions.cs @@ -64,7 +64,8 @@ namespace StardewModdingAPI.Framework /// The event to raise. public static void RaiseEmpty(this ManagedEvent @event) where TEventArgs : new() { - @event.Raise(Singleton.Instance); + if (@event.HasListeners) + @event.Raise(Singleton.Instance); } /**** diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index c1e03634..ec21e38a 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -47,6 +47,7 @@ using StardewModdingAPI.Toolkit.Utilities.PathLookups; using StardewModdingAPI.Utilities; using StardewValley; using StardewValley.Menus; +using StardewValley.Objects; using xTile.Display; using LanguageCode = StardewValley.LocalizedContentManager.LanguageCode; using MiniMonoModHotfix = MonoMod.Utils.MiniMonoModHotfix; @@ -808,7 +809,8 @@ namespace StardewModdingAPI.Framework if (verbose) this.Monitor.Log($"Events: window size changed to {state.WindowSize.New}."); - events.WindowResized.Raise(new WindowResizedEventArgs(state.WindowSize.Old, state.WindowSize.New)); + if (events.WindowResized.HasListeners) + events.WindowResized.Raise(new WindowResizedEventArgs(state.WindowSize.Old, state.WindowSize.New)); } /********* @@ -823,7 +825,7 @@ namespace StardewModdingAPI.Framework ICursorPosition cursor = instance.Input.CursorPosition; // raise cursor moved event - if (state.Cursor.IsChanged) + if (state.Cursor.IsChanged && events.CursorMoved.HasListeners) events.CursorMoved.Raise(new CursorMovedEventArgs(state.Cursor.Old!, state.Cursor.New!)); // raise mouse wheel scrolled @@ -831,32 +833,42 @@ namespace StardewModdingAPI.Framework { if (verbose) this.Monitor.Log($"Events: mouse wheel scrolled to {state.MouseWheelScroll.New}."); - events.MouseWheelScrolled.Raise(new MouseWheelScrolledEventArgs(cursor, state.MouseWheelScroll.Old, state.MouseWheelScroll.New)); + + if (events.MouseWheelScrolled.HasListeners) + events.MouseWheelScrolled.Raise(new MouseWheelScrolledEventArgs(cursor, state.MouseWheelScroll.Old, state.MouseWheelScroll.New)); } // raise input button events if (inputState.ButtonStates.Count > 0) { - events.ButtonsChanged.Raise(new ButtonsChangedEventArgs(cursor, inputState)); - - foreach (var pair in inputState.ButtonStates) - { - SButton button = pair.Key; - SButtonState status = pair.Value; + if (events.ButtonsChanged.HasListeners) + events.ButtonsChanged.Raise(new ButtonsChangedEventArgs(cursor, inputState)); - if (status == SButtonState.Pressed) - { - if (verbose) - this.Monitor.Log($"Events: button {button} pressed."); + bool raisePressed = events.ButtonPressed.HasListeners; + bool raiseReleased = events.ButtonReleased.HasListeners; - events.ButtonPressed.Raise(new ButtonPressedEventArgs(button, cursor, inputState)); - } - else if (status == SButtonState.Released) + if (verbose || raisePressed || raiseReleased) + { + foreach ((SButton button, SButtonState status) in inputState.ButtonStates) { - if (verbose) - this.Monitor.Log($"Events: button {button} released."); - - events.ButtonReleased.Raise(new ButtonReleasedEventArgs(button, cursor, inputState)); + switch (status) + { + case SButtonState.Pressed: + if (verbose) + this.Monitor.Log($"Events: button {button} pressed."); + + if (raisePressed) + events.ButtonPressed.Raise(new ButtonPressedEventArgs(button, cursor, inputState)); + break; + + case SButtonState.Released: + if (verbose) + this.Monitor.Log($"Events: button {button} released."); + + if (raiseReleased) + events.ButtonReleased.Raise(new ButtonReleasedEventArgs(button, cursor, inputState)); + break; + } } } } @@ -868,14 +880,15 @@ namespace StardewModdingAPI.Framework *********/ if (state.ActiveMenu.IsChanged) { - var was = state.ActiveMenu.Old; - var now = state.ActiveMenu.New; + IClickableMenu? was = state.ActiveMenu.Old; + IClickableMenu? now = state.ActiveMenu.New; if (verbose) this.Monitor.Log($"Context: menu changed from {was?.GetType().FullName ?? "none"} to {now?.GetType().FullName ?? "none"}."); // raise menu events - events.MenuChanged.Raise(new MenuChangedEventArgs(was, now)); + if (events.MenuChanged.HasListeners) + events.MenuChanged.Raise(new MenuChangedEventArgs(was, now)); } /********* @@ -898,7 +911,8 @@ namespace StardewModdingAPI.Framework this.Monitor.Log($"Context: location list changed (added {addedText}; removed {removedText})."); } - events.LocationListChanged.Raise(new LocationListChangedEventArgs(added, removed)); + if (events.LocationListChanged.HasListeners) + events.LocationListChanged.Raise(new LocationListChangedEventArgs(added, removed)); } // raise location contents changed @@ -906,50 +920,47 @@ namespace StardewModdingAPI.Framework { foreach (LocationSnapshot locState in state.Locations.Locations) { - var location = locState.Location; + GameLocation location = locState.Location; // buildings changed - if (locState.Buildings.IsChanged) + if (locState.Buildings.IsChanged && events.BuildingListChanged.HasListeners) events.BuildingListChanged.Raise(new BuildingListChangedEventArgs(location, locState.Buildings.Added, locState.Buildings.Removed)); // debris changed - if (locState.Debris.IsChanged) + if (locState.Debris.IsChanged && events.DebrisListChanged.HasListeners) events.DebrisListChanged.Raise(new DebrisListChangedEventArgs(location, locState.Debris.Added, locState.Debris.Removed)); // large terrain features changed - if (locState.LargeTerrainFeatures.IsChanged) + if (locState.LargeTerrainFeatures.IsChanged && events.LargeTerrainFeatureListChanged.HasListeners) events.LargeTerrainFeatureListChanged.Raise(new LargeTerrainFeatureListChangedEventArgs(location, locState.LargeTerrainFeatures.Added, locState.LargeTerrainFeatures.Removed)); // NPCs changed - if (locState.Npcs.IsChanged) + if (locState.Npcs.IsChanged && events.NpcListChanged.HasListeners) events.NpcListChanged.Raise(new NpcListChangedEventArgs(location, locState.Npcs.Added, locState.Npcs.Removed)); // objects changed - if (locState.Objects.IsChanged) + if (locState.Objects.IsChanged && events.ObjectListChanged.HasListeners) events.ObjectListChanged.Raise(new ObjectListChangedEventArgs(location, locState.Objects.Added, locState.Objects.Removed)); // chest items changed if (events.ChestInventoryChanged.HasListeners) { - foreach (var pair in locState.ChestItems) - { - SnapshotItemListDiff diff = pair.Value; - events.ChestInventoryChanged.Raise(new ChestInventoryChangedEventArgs(pair.Key, location, added: diff.Added, removed: diff.Removed, quantityChanged: diff.QuantityChanged)); - } + foreach ((Chest chest, SnapshotItemListDiff diff) in locState.ChestItems) + events.ChestInventoryChanged.Raise(new ChestInventoryChangedEventArgs(chest, location, added: diff.Added, removed: diff.Removed, quantityChanged: diff.QuantityChanged)); } // terrain features changed - if (locState.TerrainFeatures.IsChanged) + if (locState.TerrainFeatures.IsChanged && events.TerrainFeatureListChanged.HasListeners) events.TerrainFeatureListChanged.Raise(new TerrainFeatureListChangedEventArgs(location, locState.TerrainFeatures.Added, locState.TerrainFeatures.Removed)); // furniture changed - if (locState.Furniture.IsChanged) + if (locState.Furniture.IsChanged && events.FurnitureListChanged.HasListeners) events.FurnitureListChanged.Raise(new FurnitureListChangedEventArgs(location, locState.Furniture.Added, locState.Furniture.Removed)); } } // raise time changed - if (raiseWorldEvents && state.Time.IsChanged) + if (raiseWorldEvents && state.Time.IsChanged && events.TimeChanged.HasListeners) events.TimeChanged.Raise(new TimeChangedEventArgs(state.Time.Old, state.Time.New)); // raise player events @@ -964,29 +975,38 @@ namespace