From 7a91cf1cd875bc62072571e1b259a10cbacdaaab Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 9 May 2022 00:18:37 -0400 Subject: update schema for Content Patcher 1.26.0 --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 98747613..23783a8a 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,6 +1,10 @@ ← [README](README.md) # Release notes +## Upcoming release +* For the web UI: + * Updated the JSON validator/schema for Content Patcher 1.26.0. + ## 3.14.2 Released 08 May 2022 for Stardew Valley 1.5.6 or later. -- cgit From eb01aa275b272774c48672a2560455d3fb902a4e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 10 May 2022 18:51:37 -0400 Subject: fix asset propagation for player sprite recolor masks --- docs/release-notes.md | 3 +++ src/SMAPI/Metadata/CoreAssetPropagator.cs | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 23783a8a..3ba11edd 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,9 @@ # Release notes ## Upcoming release +* For mod authors: + * Fixed asset propagation for player sprites not fully updating recolor masks in some cases. + * For the web UI: * Updated the JSON validator/schema for Content Patcher 1.26.0. diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 12b73515..e014f9a9 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -1017,7 +1017,9 @@ namespace StardewModdingAPI.Metadata foreach (Farmer player in players) { - this.Reflection.GetField>>?>(typeof(FarmerRenderer), "_recolorOffsets").GetValue()?.Remove(player.getTexture()); + var recolorOffsets = this.Reflection.GetField>>?>(typeof(FarmerRenderer), "_recolorOffsets").GetValue(); + recolorOffsets?.Clear(); + player.FarmerRenderer.MarkSpriteDirty(); } -- 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 --- docs/release-notes.md | 1 + src/SMAPI/Framework/ModHelpers/ContentHelper.cs | 27 ++++++++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 3ba11edd..d66fea5d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -3,6 +3,7 @@ # Release notes ## Upcoming release * For mod authors: + * Fixed error when loading a `.xnb` file through the old content API without the file extension. * Fixed asset propagation for player sprites not fully updating recolor masks in some cases. * For the web UI: 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 --- docs/release-notes.md | 3 ++ src/SMAPI/Framework/Events/ManagedEvent.cs | 81 ++++++++++++++++++++---------- src/SMAPI/Framework/SCore.cs | 3 +- 3 files changed, 60 insertions(+), 27 deletions(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index d66fea5d..7ccd466a 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,9 @@ # Release notes ## Upcoming release +* For players: + * Further improved performance in some cases. + * For mod authors: * Fixed error when loading a `.xnb` file through the old content API without the file extension. * Fixed asset propagation for player sprites not fully updating recolor masks in some cases. 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 bbe5983acdd082d2185a69e2ad37d659a298223d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 11 May 2022 21:36:45 -0400 Subject: rewrite asset operations to reduce allocations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • When raising AssetRequested, SMAPI now creates a single event args model and reuses it for each handler. • There's now a single AssetOperationGroup per asset, which tracks the loaders/editors registered by every mod for that asset. • The operation group's loader/editor lists are now used directly instead of querying them. --- docs/release-notes.md | 3 +- src/SMAPI/Events/AssetRequestedEventArgs.cs | 43 +++-- src/SMAPI/Framework/Content/AssetOperationGroup.cs | 7 +- src/SMAPI/Framework/ContentCoordinator.cs | 186 ++++++++++----------- .../ContentManagers/GameContentManager.cs | 71 ++++---- src/SMAPI/Framework/SCore.cs | 32 ++-- 6 files changed, 167 insertions(+), 175 deletions(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 7ccd466a..b35a4760 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -3,9 +3,10 @@ # Release notes ## Upcoming release * For players: - * Further improved performance in some cases. + * Reduced mods' in-game performance impact. * For mod authors: + * Refactored how event handling works under the hood, particularly the new content API. This should have no effect on mod usage. * Fixed error when loading a `.xnb` file through the old content API without the file extension. * Fixed asset propagation for player sprites not fully updating recolor masks in some cases. diff --git a/src/SMAPI/Events/AssetRequestedEventArgs.cs b/src/SMAPI/Events/AssetRequestedEventArgs.cs index d0aef1db..d6561028 100644 --- a/src/SMAPI/Events/AssetRequestedEventArgs.cs +++ b/src/SMAPI/Events/AssetRequestedEventArgs.cs @@ -14,7 +14,7 @@ namespace StardewModdingAPI.Events ** Fields *********/ /// The mod handling the event. - private readonly IModMetadata Mod; + private IModMetadata? Mod; /// Get the mod metadata for a content pack, if it's a valid content pack for the mod. private readonly Func GetOnBehalfOf; @@ -37,26 +37,31 @@ namespace StardewModdingAPI.Events public Type DataType => this.AssetInfo.DataType; /// The load operations requested by the event handler. - internal IList LoadOperations { get; } = new List(); + internal List LoadOperations { get; } = new(); /// The edit operations requested by the event handler. - internal IList EditOperations { get; } = new List(); + internal List EditOperations { get; } = new(); /********* ** Public methods *********/ /// Construct an instance. - /// The mod handling the event. /// The asset info being requested. /// Get the mod metadata for a content pack, if it's a valid content pack for the mod. - internal AssetRequestedEventArgs(IModMetadata mod, IAssetInfo assetInfo, Func getOnBehalfOf) + internal AssetRequestedEventArgs(IAssetInfo assetInfo, Func getOnBehalfOf) { - this.Mod = mod; this.AssetInfo = assetInfo; this.GetOnBehalfOf = getOnBehalfOf; } + /// Set the mod handling the event. + /// The mod handling the event. + internal void SetMod(IModMetadata mod) + { + this.Mod = mod; + } + /// Provide the initial instance for the asset, instead of trying to load it from the game's Content folder. /// Get the initial instance of an asset. /// If there are multiple loads that apply to the same asset, the priority with which this one should be applied. @@ -70,10 +75,11 @@ namespace StardewModdingAPI.Events /// public void LoadFrom(Func load, AssetLoadPriority priority, string? onBehalfOf = null) { + IModMetadata mod = this.GetMod(); this.LoadOperations.Add( new AssetLoadOperation( - Mod: this.Mod, - OnBehalfOf: this.GetOnBehalfOf(this.Mod, onBehalfOf, "load assets"), + Mod: mod, + OnBehalfOf: this.GetOnBehalfOf(mod, onBehalfOf, "load assets"), Priority: priority, GetData: _ => load() ) @@ -94,12 +100,13 @@ namespace StardewModdingAPI.Events public void LoadFromModFile(string relativePath, AssetLoadPriority priority) where TAsset : notnull { + IModMetadata mod = this.GetMod(); this.LoadOperations.Add( new AssetLoadOperation( - Mod: this.Mod, + Mod: mod, OnBehalfOf: null, Priority: priority, - GetData: _ => this.Mod.Mod!.Helper.ModContent.Load(relativePath) + GetData: _ => mod.Mod!.Helper.ModContent.Load(relativePath) ) ); } @@ -117,14 +124,26 @@ namespace StardewModdingAPI.Events /// public void Edit(Action apply, AssetEditPriority priority = AssetEditPriority.Default, string? onBehalfOf = null) { + IModMetadata mod = this.GetMod(); this.EditOperations.Add( new AssetEditOperation( - Mod: this.Mod, + Mod: mod, Priority: priority, - OnBehalfOf: this.GetOnBehalfOf(this.Mod, onBehalfOf, "edit assets"), + OnBehalfOf: this.GetOnBehalfOf(mod, onBehalfOf, "edit assets"), ApplyEdit: apply ) ); } + + + /********* + ** Private methods + *********/ + /// Get the mod handling the event. + /// This instance hasn't been initialized with the mod metadata yet. + private IModMetadata GetMod() + { + return this.Mod ?? throw new InvalidOperationException($"This {nameof(AssetRequestedEventArgs)} instance hasn't been initialized yet."); + } } } diff --git a/src/SMAPI/Framework/Content/AssetOperationGroup.cs b/src/SMAPI/Framework/Content/AssetOperationGroup.cs index 1566a8f0..11767d39 100644 --- a/src/SMAPI/Framework/Content/AssetOperationGroup.cs +++ b/src/SMAPI/Framework/Content/AssetOperationGroup.cs @@ -1,8 +1,9 @@ +using System.Collections.Generic; + namespace StardewModdingAPI.Framework.Content { - /// A set of operations to apply to an asset for a given or implementation. - /// The mod applying the changes. + /// A set of operations to apply to an asset. /// The load operations to apply. /// The edit operations to apply. - internal record AssetOperationGroup(IModMetadata Mod, AssetLoadOperation[] LoadOperations, AssetEditOperation[] EditOperations); + internal record AssetOperationGroup(List LoadOperations, List EditOperations); } diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index 6702f5e6..a24581a0 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -57,7 +57,7 @@ namespace StardewModdingAPI.Framework private readonly Action> OnAssetsInvalidated; /// Get the load/edit operations to apply to an asset by querying registered event handlers. - private readonly Func> RequestAssetOperations; + private readonly Func RequestAssetOperations; /// The loaded content managers (including the ). private readonly List ContentManagers = new(); @@ -79,15 +79,15 @@ namespace StardewModdingAPI.Framework private Lazy> LocaleCodes; /// The cached asset load/edit operations to apply, indexed by asset name. - private readonly TickCacheDictionary> AssetOperationsByKey = new(); + private readonly TickCacheDictionary AssetOperationsByKey = new(); /// A cache of asset operation groups created for legacy implementations. [Obsolete("This only exists to support legacy code and will be removed in SMAPI 4.0.0.")] - private readonly Dictionary> LegacyLoaderCache = new(ReferenceEqualityComparer.Instance); + private readonly Dictionary> LegacyLoaderCache = new(ReferenceEqualityComparer.Instance); /// A cache of asset operation groups created for legacy implementations. [Obsolete("This only exists to support legacy code and will be removed in SMAPI 4.0.0.")] - private readonly Dictionary> LegacyEditorCache = new(ReferenceEqualityComparer.Instance); + private readonly Dictionary> LegacyEditorCache = new(ReferenceEqualityComparer.Instance); /********* @@ -126,7 +126,7 @@ namespace StardewModdingAPI.Framework /// Get a file lookup for the given directory. /// A callback to invoke when any asset names have been invalidated from the cache. /// Get the load/edit operations to apply to an asset by querying registered event handlers. - public ContentCoordinator(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action onLoadingFirstAsset, Action onAssetLoaded, Func getFileLookup, Action> onAssetsInvalidated, Func> requestAssetOperations) + public ContentCoordinator(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action onLoadingFirstAsset, Action onAssetLoaded, Func getFileLookup, Action> onAssetsInvalidated, Func requestAssetOperations) { this.GetFileLookup = getFileLookup; this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor)); @@ -449,16 +449,12 @@ namespace StardewModdingAPI.Framework /// Get the asset load and edit operations to apply to a given asset if it's (re)loaded now. /// The asset type. /// The asset info to load or edit. - public IList GetAssetOperations(IAssetInfo info) + public AssetOperationGroup? GetAssetOperations(IAssetInfo info) where T : notnull { return this.AssetOperationsByKey.GetOrSet( info.Name, -#pragma warning disable CS0612, CS0618 // deprecated code - () => this.Editors.Count > 0 || this.Loaders.Count > 0 - ? this.GetAssetOperationsIncludingLegacyWithoutCache(info).ToArray() -#pragma warning restore CS0612, CS0618 - : this.RequestAssetOperations(info) + () => this.GetAssetOperationsWithoutCache(info) ); } @@ -584,41 +580,40 @@ namespace StardewModdingAPI.Framework /// Get the asset load and edit operations to apply to a given asset if it's (re)loaded now, ignoring the cache. /// The asset type. /// The asset info to load or edit. - [Obsolete("This only exists to support legacy code and will be removed in SMAPI 4.0.0.")] - private IEnumerable GetAssetOperationsIncludingLegacyWithoutCache(IAssetInfo info) + private AssetOperationGroup? GetAssetOperationsWithoutCache(IAssetInfo info) where T : notnull { - IAssetInfo legacyInfo = this.GetLegacyAssetInfo(info); - // new content API - foreach (AssetOperationGroup group in this.RequestAssetOperations(info)) - yield return group; + AssetOperationGroup? group = this.RequestAssetOperations(info); // legacy load operations - foreach (ModLinked loader in this.Loaders) +#pragma warning disable CS0612, CS0618 // deprecated code + if (this.Editors.Count > 0 || this.Loaders.Count > 0) { - // check if loader applies - try + IAssetInfo legacyInfo = this.GetLegacyAssetInfo(info); + + foreach (ModLinked loader in this.Loaders) { - if (!loader.Data.CanLoad(legacyInfo)) + // check if loader applies + try + { + if (!loader.Data.CanLoad(legacyInfo)) + continue; + } + catch (Exception ex) + { + loader.Mod.LogAsMod($"Mod failed when checking whether it could load asset '{legacyInfo.Name}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); continue; - } - catch (Exception ex) - { - loader.Mod.LogAsMod($"Mod failed when checking whether it could load asset '{legacyInfo.Name}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); - continue; - } + } - // add operation - yield return this.GetOrCreateLegacyOperationGroup( - cache: this.LegacyLoaderCache, - editor: loader.Data, - dataType: info.DataType, - createGroup: () => new AssetOperationGroup( - Mod: loader.Mod, - LoadOperations: new[] - { - new AssetLoadOperation( + // add operation + group ??= new AssetOperationGroup(new List(), new List()); + group.LoadOperations.Add( + this.GetOrCreateLegacyOperation( + cache: this.LegacyLoaderCache, + editor: loader.Data, + dataType: info.DataType, + create: () => new AssetLoadOperation( Mod: loader.Mod, OnBehalfOf: null, Priority: AssetLoadPriority.Exclusive, @@ -626,59 +621,54 @@ namespace StardewModdingAPI.Framework this.GetLegacyAssetInfo(assetInfo) ) ) - }, - EditOperations: Array.Empty() - ) - ); - } + ) + ); + } - // legacy edit operations - foreach (var editor in this.Editors) - { - // check if editor applies - try + // legacy edit operations + foreach (var editor in this.Editors) { - if (!editor.Data.CanEdit(legacyInfo)) + // check if editor applies + try + { + if (!editor.Data.CanEdit(legacyInfo)) + continue; + } + catch (Exception ex) + { + editor.Mod.LogAsMod($"Mod crashed when checking whether it could edit asset '{legacyInfo.Name}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); continue; - } - catch (Exception ex) - { - editor.Mod.LogAsMod($"Mod crashed when checking whether it could edit asset '{legacyInfo.Name}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); - continue; - } + } - // HACK - // - // If two editors have the same priority, they're applied in registration order (so - // whichever was registered first is applied first). Mods often depend on this - // behavior, like Json Assets registering its interceptors before Content Patcher. - // - // Unfortunately the old & new content APIs have separate lists, so new-API - // interceptors always ran before old-API interceptors with the same priority, - // regardless of the registration order *between* APIs. Since the new API works in - // a fundamentally different way (i.e. loads/edits are defined on asset request - // instead of by registering a global 'editor' or 'loader' class), there's no way - // to track registration order between them. - // - // Until we drop the old content API in SMAPI 4.0.0, this sets the priority for - // specific legacy editors to maintain compatibility. - AssetEditPriority priority = editor.Data.GetType().FullName switch - { - "JsonAssets.Framework.ContentInjector1" => AssetEditPriority.Default - 1, // must be applied before Content Patcher - _ => AssetEditPriority.Default - }; - - // add operation - yield return this.GetOrCreateLegacyOperationGroup( - cache: this.LegacyEditorCache, - editor: editor.Data, - dataType: info.DataType, - createGroup: () => new AssetOperationGroup( - Mod: editor.Mod, - LoadOperations: Array.Empty(), - EditOperations: new[] - { - new AssetEditOperation( + // HACK + // + // If two editors have the same priority, they're applied in registration order (so + // whichever was registered first is applied first). Mods often depend on this + // behavior, like Json Assets registering its interceptors before Content Patcher. + // + // Unfortunately the old & new content APIs have separate lists, so new-API + // interceptors always ran before old-API interceptors with the same priority, + // regardless of the registration order *between* APIs. Since the new API works in + // a fundamentally different way (i.e. loads/edits are defined on asset request + // instead of by registering a global 'editor' or 'loader' class), there's no way + // to track registration order between them. + // + // Until we drop the old content API in SMAPI 4.0.0, this sets the priority for + // specific legacy editors to maintain compatibility. + AssetEditPriority priority = editor.Data.GetType().FullName switch + { + "JsonAssets.Framework.ContentInjector1" => AssetEditPriority.Default - 1, // must be applied before Content Patcher + _ => AssetEditPriority.Default + }; + + // add operation + group ??= new AssetOperationGroup(new List(), new List()); + group.EditOperations.Add( + this.GetOrCreateLegacyOperation( + cache: this.LegacyEditorCache, + editor: editor.Data, + dataType: info.DataType, + create: () => new AssetEditOperation( Mod: editor.Mod, OnBehalfOf: null, Priority: priority, @@ -686,28 +676,32 @@ namespace StardewModdingAPI.Framework this.GetLegacyAssetData(assetData) ) ) - } - ) - ); + ) + ); + } } +#pragma warning restore CS0612, CS0618 + + return group; } /// Get a cached asset operation group for a legacy or instance, creating it if needed. /// The editor type (one of or ). + /// The operation model type. /// The cached operation groups for the interceptor type. /// The legacy asset interceptor. /// The asset data type. - /// Create the asset operation group if it's not cached yet. - private AssetOperationGroup GetOrCreateLegacyOperationGroup(Dictionary> cache, TInterceptor editor, Type dataType, Func createGroup) + /// Create the asset operation group if it's not cached yet. + private TOperation GetOrCreateLegacyOperation(Dictionary> cache, TInterceptor editor, Type dataType, Func create) where TInterceptor : class { - if (!cache.TryGetValue(editor, out Dictionary? cacheByType)) - cache[editor] = cacheByType = new Dictionary(); + if (!cache.TryGetValue(editor, out Dictionary? cacheByType)) + cache[editor] = cacheByType = new Dictionary(); - if (!cacheByType.TryGetValue(dataType, out AssetOperationGroup? group)) - cacheByType[dataType] = group = createGroup(); + if (!cacheByType.TryGetValue(dataType, out TOperation? operation)) + cacheByType[dataType] = operation = create(); - return group; + return operation; } /// Get an asset info compatible with legacy and instances, which always expect the base name. diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index c53040e1..2aa50542 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -75,15 +75,19 @@ namespace StardewModdingAPI.Framework.ContentManagers // custom asset from a loader string locale = this.GetLocale(); IAssetInfo info = new AssetInfo(locale, assetName, typeof(T), this.AssertAndNormalizeAssetName); - AssetLoadOperation[] loaders = this.GetLoaders(info).ToArray(); - - if (!this.AssertMaxOneRequiredLoader(info, loaders, out string? error)) + AssetOperationGroup? operations = this.Coordinator.GetAssetOperations(info); + if (operations?.LoadOperations.Count > 0) { - this.Monitor.Log(error, LogLevel.Warn); - return false; + if (!this.AssertMaxOneRequiredLoader(info, operations.LoadOperations, out string? error)) + { + this.Monitor.Log(error, LogLevel.Warn); + return false; + } + + return true; } - return loaders.Any(); + return false; } /// @@ -121,10 +125,11 @@ namespace StardewModdingAPI.Framework.ContentManagers data = this.AssetsBeingLoaded.Track(assetName.Name, () => { IAssetInfo info = new AssetInfo(assetName.LocaleCode, assetName, typeof(T), this.AssertAndNormalizeAssetName); + AssetOperationGroup? operations = this.Coordinator.GetAssetOperations(info); IAssetData asset = - this.ApplyLoader(info) + this.ApplyLoader(info, operations?.LoadOperations) ?? new AssetDataForObject(info, this.RawLoad(assetName, useCache), this.AssertAndNormalizeAssetName, this.Reflection); - asset = this.ApplyEditors(info, asset); + asset = this.ApplyEditors(info, asset, operations?.EditOperations); return (T)asset.Data; }); } @@ -149,25 +154,23 @@ namespace StardewModdingAPI.Framework.ContentManagers *********/ /// Load the initial asset from the registered loaders. /// The basic asset metadata. + /// The load operations to apply to the asset. /// Returns the loaded asset metadata, or null if no loader matched. - private IAssetData? ApplyLoader(IAssetInfo info) + private IAssetData? ApplyLoader(IAssetInfo info, List? loadOperations) where T : notnull { // find matching loader - AssetLoadOperation? loader; + AssetLoadOperation? loader = null; + if (loadOperations?.Count > 0) { - AssetLoadOperation[] loaders = this.GetLoaders(info).OrderByDescending(p => p.Priority).ToArray(); - - if (!this.AssertMaxOneRequiredLoader(info, loaders, out string? error)) + if (!this.AssertMaxOneRequiredLoader(info, loadOperations, out string? error)) { this.Monitor.Log(error, LogLevel.Warn); return null; } - loader = loaders.FirstOrDefault(); + loader = loadOperations.OrderByDescending(p => p.Priority).FirstOrDefault(); } - - // no loader found if (loader == null) return null; @@ -195,9 +198,13 @@ namespace StardewModdingAPI.Framework.ContentManagers /// The asset type. /// The basic asset metadata. /// The loaded asset. - private IAssetData ApplyEditors(IAssetInfo info, IAssetData asset) + /// The edit operations to apply to the asset. + private IAssetData ApplyEditors(IAssetInfo info, IAssetData asset, List? editOperations) where T : notnull { + if (editOperations?.Count is not > 0) + return asset; + IAssetData GetNewData(object data) => new AssetDataForObject(info, data, this.AssertAndNormalizeAssetName, this.Reflection); // special case: if the asset was loaded with a more general type like 'object', call editors with the actual type instead. @@ -210,12 +217,12 @@ namespace StardewModdingAPI.Framework.ContentManagers return (IAssetData)this.GetType() .GetMethod(nameof(this.ApplyEditors), BindingFlags.NonPublic | BindingFlags.Instance)! .MakeGenericMethod(actualType) - .Invoke(this, new object[] { info, asset })!; + .Invoke(this, new object[] { info, asset, editOperations })!; } } // edit asset - AssetEditOperation[] editors = this.GetEditors(info).OrderBy(p => p.Priority).ToArray(); + AssetEditOperation[] editors = editOperations.OrderBy(p => p.Priority).ToArray(); foreach (AssetEditOperation editor in editors) { IModMetadata mod = editor.Mod; @@ -250,34 +257,12 @@ namespace StardewModdingAPI.Framework.ContentManagers return asset; } - /// Get the asset loaders which handle an asset. - /// The asset type. - /// The basic asset metadata. - private IEnumerable GetLoaders(IAssetInfo info) - where T : notnull - { - return this.Coordinator - .GetAssetOperations(info) - .SelectMany(p => p.LoadOperations); - } - - /// Get the asset editors to apply to an asset. - /// The asset type. - /// The basic asset metadata. - private IEnumerable GetEditors(IAssetInfo info) - where T : notnull - { - return this.Coordinator - .GetAssetOperations(info) - .SelectMany(p => p.EditOperations); - } - /// Assert that at most one loader will be applied to an asset. /// The basic asset metadata. /// The asset loaders to apply. /// The error message to show to the user, if the method returns false. /// Returns true if only one loader will apply, else false. - private bool AssertMaxOneRequiredLoader(IAssetInfo info, AssetLoadOperation[] loaders, [NotNullWhen(false)] out string? error) + private bool AssertMaxOneRequiredLoader(IAssetInfo info, List loaders, [NotNullWhen(false)] out string? error) { AssetLoadOperation[] required = loaders.Where(p => p.Priority == AssetLoadPriority.Exclusive).ToArray(); if (required.Length <= 1) @@ -295,7 +280,7 @@ namespace StardewModdingAPI.Framework.ContentManagers ? $"Multiple mods want to provide the '{info.Name}' asset: {string.Join(", ", loaderNames)}" : $"The '{loaderNames[0]}' mod wants to provide the '{info.Name}' asset multiple times"; - error = $"{errorPhrase}. An asset can't be loaded multiple times, so SMAPI will use the default asset instead. Uninstall one of the mods to fix this. (Message for modders: you should usually use {typeof(IAssetEditor)} instead to avoid conflicts.)"; + error = $"{errorPhrase}. An asset can't be loaded multiple times, so SMAPI will use the default asset instead. Uninstall one of the mods to fix this. (Message for modders: you should avoid {nameof(AssetLoadPriority)}.{nameof(AssetLoadPriority.Exclusive)} and {nameof(IAssetLoader)} if possible to avoid conflicts.)"; return false; } diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index ec21e38a..41b975e8 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -1178,35 +1178,27 @@ namespace StardewModdingAPI.Framework /// Get the load/edit operations to apply to an asset by querying registered event handlers. /// The asset info being requested. - private IList RequestAssetOperations(IAssetInfo asset) + private AssetOperationGroup? RequestAssetOperations(IAssetInfo asset) { // get event - var @event = this.EventManager.AssetRequested; - if (!@event.HasListeners) - return Array.Empty(); + var requestedEvent = this.EventManager.AssetRequested; + if (!requestedEvent.HasListeners) + return null; - // get operations - List? operations = null; - @event.Raise( + // raise event + AssetRequestedEventArgs args = new(asset, this.GetOnBehalfOfContentPack); + requestedEvent.Raise( invoke: (mod, invoke) => { - AssetRequestedEventArgs args = new(mod, asset, this.GetOnBehalfOfContentPack); - + args.SetMod(mod); invoke(args); - - if (args.LoadOperations.Any() || args.EditOperations.Any()) - { - operations ??= new(); - operations.Add( - new AssetOperationGroup(mod, args.LoadOperations.ToArray(), args.EditOperations.ToArray()) - ); - } } ); - return operations != null - ? operations - : Array.Empty(); + // collect operations + return args.LoadOperations.Count != 0 || args.EditOperations.Count != 0 + ? new AssetOperationGroup(args.LoadOperations, args.EditOperations) + : null; } /// Get the mod metadata for a content pack whose ID matches , if it's a valid content pack for the given . -- cgit From 5a7422b3122dc0c46c6fe9b445eae0bc5df77298 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 11 May 2022 22:32:33 -0400 Subject: log time change in verbose mode --- docs/release-notes.md | 1 + src/SMAPI/Framework/SCore.cs | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index b35a4760..53711454 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -7,6 +7,7 @@ * For mod authors: * Refactored how event handling works under the hood, particularly the new content API. This should have no effect on mod usage. + * In-game time changes are now logged in verbose mod. * Fixed error when loading a `.xnb` file through the old content API without the file extension. * Fixed asset propagation for player sprites not fully updating recolor masks in some cases. diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 41b975e8..5ae4fdbb 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -960,8 +960,14 @@ namespace StardewModdingAPI.Framework } // raise time changed - if (raiseWorldEvents && state.Time.IsChanged && events.TimeChanged.HasListeners) - events.TimeChanged.Raise(new TimeChangedEventArgs(state.Time.Old, state.Time.New)); + if (raiseWorldEvents && state.Time.IsChanged) + { + if (verbose) + this.Monitor.Log($"Context: time changed to {state.Time.New}."); + + if (events.TimeChanged.HasListeners) + events.TimeChanged.Raise(new TimeChangedEventArgs(state.Time.Old, state.Time.New)); + } // raise player events if (raiseWorldEvents) -- cgit From e943ae84136d46432e04e577041850d2aa7db43e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 12 May 2022 00:21:46 -0400 Subject: prepare for release --- build/common.targets | 2 +- docs/release-notes.md | 8 +++++--- src/SMAPI.Mods.ConsoleCommands/manifest.json | 4 ++-- src/SMAPI.Mods.ErrorHandler/manifest.json | 4 ++-- src/SMAPI.Mods.SaveBackup/manifest.json | 4 ++-- src/SMAPI/Constants.cs | 2 +- 6 files changed, 13 insertions(+), 11 deletions(-) (limited to 'docs') diff --git a/build/common.targets b/build/common.targets index 0436ed5b..a7db917c 100644 --- a/build/common.targets +++ b/build/common.targets @@ -1,7 +1,7 @@ - 3.14.2 + 3.14.3 SMAPI latest $(AssemblySearchPaths);{GAC} diff --git a/docs/release-notes.md b/docs/release-notes.md index 53711454..6311d7dc 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,13 +1,15 @@ ← [README](README.md) # Release notes -## Upcoming release +## 3.14.3 +Released 12 May 2022 for Stardew Valley 1.5.6 or later. + * For players: - * Reduced mods' in-game performance impact. + * Reduced in-game performance impact. * For mod authors: * Refactored how event handling works under the hood, particularly the new content API. This should have no effect on mod usage. - * In-game time changes are now logged in verbose mod. + * Verbose mode now logs the in-game time. * Fixed error when loading a `.xnb` file through the old content API without the file extension. * Fixed asset propagation for player sprites not fully updating recolor masks in some cases. diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index c263456a..0e2b023d 100644 --- a/src/SMAPI.Mods.ConsoleCommands/manifest.json +++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json @@ -1,9 +1,9 @@ { "Name": "Console Commands", "Author": "SMAPI", - "Version": "3.14.2", + "Version": "3.14.3", "Description": "Adds SMAPI console commands that let you manipulate the game.", "UniqueID": "SMAPI.ConsoleCommands", "EntryDll": "ConsoleCommands.dll", - "MinimumApiVersion": "3.14.2" + "MinimumApiVersion": "3.14.3" } diff --git a/src/SMAPI.Mods.ErrorHandler/manifest.json b/src/SMAPI.Mods.ErrorHandler/manifest.json index 6e6a271f..f449b3bd 100644 --- a/src/SMAPI.Mods.ErrorHandler/manifest.json +++ b/src/SMAPI.Mods.ErrorHandler/manifest.json @@ -1,9 +1,9 @@ { "Name": "Error Handler", "Author": "SMAPI", - "Version": "3.14.2", + "Version": "3.14.3", "Description": "Handles some common vanilla errors to log more useful info or avoid breaking the game.", "UniqueID": "SMAPI.ErrorHandler", "EntryDll": "ErrorHandler.dll", - "MinimumApiVersion": "3.14.2" + "MinimumApiVersion": "3.14.3" } diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json index 5ba91568..23e241b5 100644 --- a/src/SMAPI.Mods.SaveBackup/manifest.json +++ b/src/SMAPI.Mods.SaveBackup/manifest.json @@ -1,9 +1,9 @@ { "Name": "Save Backup", "Author": "SMAPI", - "Version": "3.14.2", + "Version": "3.14.3", "Description": "Automatically backs up all your saves once per day into its folder.", "UniqueID": "SMAPI.SaveBackup", "EntryDll": "SaveBackup.dll", - "MinimumApiVersion": "3.14.2" + "MinimumApiVersion": "3.14.3" } diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index a289ce4b..ef729f4f 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -50,7 +50,7 @@ namespace StardewModdingAPI internal static int? LogScreenId { get; set; } /// SMAPI's current raw semantic version. - internal static string RawApiVersion = "3.14.2"; + internal static string RawApiVersion = "3.14.3"; } /// Contains SMAPI's constants and assumptions. -- cgit