From 3a161a30a7faa2d69ebe08938cd68f43921b4a81 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 18 May 2022 19:23:26 -0400 Subject: update for the new CurseForge API --- .../Clients/CurseForge/CurseForgeClient.cs | 25 +++++++++---- .../CurseForge/ResponseModels/ModLinksModel.cs | 7 ++++ .../Clients/CurseForge/ResponseModels/ModModel.cs | 41 ++++------------------ .../CurseForge/ResponseModels/ResponseModel.cs | 8 +++++ .../Framework/ConfigModels/ApiClientsConfig.cs | 3 ++ src/SMAPI.Web/Startup.cs | 3 +- src/SMAPI.Web/appsettings.json | 3 +- 7 files changed, 46 insertions(+), 44 deletions(-) create mode 100644 src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModLinksModel.cs create mode 100644 src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ResponseModel.cs (limited to 'src') diff --git a/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs b/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs index d351b42d..9b4f2580 100644 --- a/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs +++ b/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Net; using System.Text.RegularExpressions; using System.Threading.Tasks; using Pathoschild.Http.Client; @@ -33,9 +34,12 @@ namespace StardewModdingAPI.Web.Framework.Clients.CurseForge /// Construct an instance. /// The user agent for the API client. /// The base URL for the CurseForge API. - public CurseForgeClient(string userAgent, string apiUrl) + /// The API authentication key. + public CurseForgeClient(string userAgent, string apiUrl, string apiKey) { - this.Client = new FluentClient(apiUrl).SetUserAgent(userAgent); + this.Client = new FluentClient(apiUrl) + .SetUserAgent(userAgent) + .AddDefault(request => request.WithHeader("x-api-key", apiKey)); } /// Get update check info about a mod. @@ -49,11 +53,18 @@ namespace StardewModdingAPI.Web.Framework.Clients.CurseForge return page.SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid CurseForge mod ID, must be an integer ID."); // get raw data - ModModel? mod = await this.Client - .GetAsync($"addon/{parsedId}") - .As(); - if (mod == null) + ModModel? mod; + try + { + ResponseModel response = await this.Client + .GetAsync($"mods/{parsedId}") + .As>(); + mod = response.Data; + } + catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound) + { return page.SetError(RemoteModStatus.DoesNotExist, "Found no CurseForge mod with this ID."); + } // get downloads List downloads = new List(); @@ -65,7 +76,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.CurseForge } // return info - return page.SetInfo(name: mod.Name, version: null, url: mod.WebsiteUrl, downloads: downloads); + return page.SetInfo(name: mod.Name, version: null, url: mod.Links.WebsiteUrl, downloads: downloads); } /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. diff --git a/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModLinksModel.cs b/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModLinksModel.cs new file mode 100644 index 00000000..2f9abe4f --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModLinksModel.cs @@ -0,0 +1,7 @@ +namespace StardewModdingAPI.Web.Framework.Clients.CurseForge.ResponseModels +{ + /// A list of links for a mod. + /// The URL for the CurseForge mod page. + /// The URL for the mod's source code, if any. + public record ModLinksModel(string WebsiteUrl, string? SourceUrl); +} diff --git a/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModModel.cs b/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModModel.cs index fd7796f2..7018be54 100644 --- a/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModModel.cs +++ b/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModModel.cs @@ -1,38 +1,9 @@ namespace StardewModdingAPI.Web.Framework.Clients.CurseForge.ResponseModels { - /// An mod from the CurseForge API. - public class ModModel - { - /********* - ** Accessors - *********/ - /// The mod's unique ID on CurseForge. - public int ID { get; } - - /// The mod name. - public string Name { get; } - - /// The web URL for the mod page. - public string WebsiteUrl { get; } - - /// The available file downloads. - public ModFileModel[] LatestFiles { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The mod's unique ID on CurseForge. - /// The mod name. - /// The web URL for the mod page. - /// The available file downloads. - public ModModel(int id, string name, string websiteUrl, ModFileModel[] latestFiles) - { - this.ID = id; - this.Name = name; - this.WebsiteUrl = websiteUrl; - this.LatestFiles = latestFiles; - } - } + /// A mod from the CurseForge API. + /// The mod's unique ID on CurseForge. + /// The mod name. + /// The available file downloads. + /// The URLs for this mod. + public record ModModel(int Id, string Name, ModFileModel[] LatestFiles, ModLinksModel Links); } diff --git a/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ResponseModel.cs b/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ResponseModel.cs new file mode 100644 index 00000000..4d538a93 --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ResponseModel.cs @@ -0,0 +1,8 @@ +using Newtonsoft.Json; + +namespace StardewModdingAPI.Web.Framework.Clients.CurseForge.ResponseModels +{ + /// A response from the CurseForge API. + /// The data returned by the API. + public record ResponseModel(TData Data); +} diff --git a/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs index b582b2b0..ebb3618a 100644 --- a/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs +++ b/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs @@ -42,6 +42,9 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels /// The base URL for the CurseForge API. public string CurseForgeBaseUrl { get; set; } = null!; + /// The API authentication key for the CurseForge API. + public string CurseForgeApiKey { get; set; } = null!; + /**** ** GitHub diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index 2693aa90..9980d00c 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -111,7 +111,8 @@ namespace StardewModdingAPI.Web services.AddSingleton(new CurseForgeClient( userAgent: userAgent, - apiUrl: api.CurseForgeBaseUrl + apiUrl: api.CurseForgeBaseUrl, + apiKey: api.CurseForgeApiKey )); services.AddSingleton(new GitHubClient( diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json index 1231f824..10fcbe9c 100644 --- a/src/SMAPI.Web/appsettings.json +++ b/src/SMAPI.Web/appsettings.json @@ -31,7 +31,8 @@ "ChucklefishBaseUrl": "https://community.playstarbound.com", "ChucklefishModPageUrlFormat": "resources/{0}", - "CurseForgeBaseUrl": "https://addons-ecs.forgesvc.net/api/v2/", + "CurseForgeBaseUrl": "https://api.curseforge.com/v1/", + "CurseForgeApiKey": null, "GitHubBaseUrl": "https://api.github.com", "GitHubAcceptHeader": "application/vnd.github.v3+json", -- cgit From cb11f1e2ca907cf9d99d5ff76c40b2d0b2957ceb Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 18 May 2022 20:02:12 -0400 Subject: re-add internal content manager for asset propagation This will be used by the new asset propagation in SMAPI 4.0 & Stardew Valley 1.6. --- src/SMAPI/Framework/ContentCoordinator.cs | 17 ++++++++++++++++- src/SMAPI/Metadata/CoreAssetPropagator.cs | 8 +++++++- 2 files changed, 23 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index 2b13f57a..da8f0da9 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -151,8 +151,23 @@ namespace StardewModdingAPI.Framework onAssetLoaded: onAssetLoaded ) ); + + var contentManagerForAssetPropagation = new GameContentManagerForAssetPropagation( + name: nameof(GameContentManagerForAssetPropagation), + serviceProvider: serviceProvider, + rootDirectory: rootDirectory, + currentCulture: currentCulture, + coordinator: this, + monitor: monitor, + reflection: reflection, + onDisposing: this.OnDisposing, + onLoadingFirstAsset: onLoadingFirstAsset, + onAssetLoaded: onAssetLoaded + ); + this.ContentManagers.Add(contentManagerForAssetPropagation); + this.VanillaContentManager = new LocalizedContentManager(serviceProvider, rootDirectory); - this.CoreAssets = new CoreAssetPropagator(this.MainContentManager, this.Monitor, reflection, name => this.ParseAssetName(name, allowLocales: true)); + this.CoreAssets = new CoreAssetPropagator(this.MainContentManager, contentManagerForAssetPropagation, this.Monitor, reflection, name => this.ParseAssetName(name, allowLocales: true)); this.LocaleCodes = new Lazy>(() => this.GetLocaleCodes(customLanguages: Enumerable.Empty())); } diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index e014f9a9..6cb5a73a 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI.Framework.ContentManagers; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Internal; using StardewModdingAPI.Toolkit.Utilities; @@ -32,6 +33,9 @@ namespace StardewModdingAPI.Metadata /// The main content manager through which to reload assets. private readonly LocalizedContentManager MainContentManager; + /// An internal content manager used only for asset propagation. See remarks on . + private readonly GameContentManagerForAssetPropagation DisposableContentManager; + /// Writes messages to the console. private readonly IMonitor Monitor; @@ -60,12 +64,14 @@ namespace StardewModdingAPI.Metadata *********/ /// Initialize the core asset data. /// The main content manager through which to reload assets. + /// An internal content manager used only for asset propagation. /// Writes messages to the console. /// Simplifies access to private code. /// Parse a raw asset name. - public CoreAssetPropagator(LocalizedContentManager mainContent, IMonitor monitor, Reflector reflection, Func parseAssetName) + public CoreAssetPropagator(LocalizedContentManager mainContent, GameContentManagerForAssetPropagation disposableContent, IMonitor monitor, Reflector reflection, Func parseAssetName) { this.MainContentManager = mainContent; + this.DisposableContentManager = disposableContent; this.Monitor = monitor; this.Reflection = reflection; this.ParseAssetName = parseAssetName; -- cgit From f8b62e271e5019fd02c4cbd0cdeb8a3b7938c620 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 18 May 2022 20:04:51 -0400 Subject: fix asset type when checking if a mod asset exists --- src/SMAPI/Framework/ContentManagers/GameContentManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index 1c603f85..4390d472 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -75,7 +75,7 @@ namespace StardewModdingAPI.Framework.ContentManagers // custom asset from a loader string locale = this.GetLocale(); IAssetInfo info = new AssetInfo(locale, assetName, typeof(T), this.AssertAndNormalizeAssetName); - AssetOperationGroup? operations = this.Coordinator.GetAssetOperations(info); + AssetOperationGroup? operations = this.Coordinator.GetAssetOperations(info); if (operations?.LoadOperations.Count > 0) { if (!this.AssertMaxOneRequiredLoader(info, operations.LoadOperations, out string? error)) -- cgit From 1ddf70697eda8b721617c023cd827fa6ac0759c4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 18 May 2022 20:13:09 -0400 Subject: simplify asset propagation a bit to prepare for the upcoming SDV 1.6 --- src/SMAPI/Framework/ContentCoordinator.cs | 7 +++++- .../ContentManagers/BaseContentManager.cs | 27 ++++++++++------------ .../Framework/ContentManagers/IContentManager.cs | 11 +++++---- 3 files changed, 25 insertions(+), 20 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index da8f0da9..dd3d2917 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -394,9 +394,14 @@ namespace StardewModdingAPI.Framework // cached assets foreach (IContentManager contentManager in this.ContentManagers) { - foreach ((string key, object asset) in contentManager.InvalidateCache((key, type) => predicate(contentManager, key, type), dispose)) + foreach ((string key, object asset) in contentManager.GetCachedAssets()) { + if (!predicate(contentManager, key, asset.GetType())) + continue; + AssetName assetName = this.ParseAssetName(key, allowLocales: true); + contentManager.InvalidateCache(assetName, dispose); + if (!invalidatedAssets.ContainsKey(assetName)) invalidatedAssets[assetName] = asset.GetType(); } diff --git a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs index 575d252e..ddc02a8c 100644 --- a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs @@ -231,24 +231,21 @@ namespace StardewModdingAPI.Framework.ContentManagers ** Cache invalidation ****/ /// - public IDictionary InvalidateCache(Func predicate, bool dispose = false) + public IEnumerable> GetCachedAssets() { - IDictionary removeAssets = new Dictionary(StringComparer.OrdinalIgnoreCase); - this.Cache.Remove((key, asset) => - { - string baseAssetName = this.Coordinator.ParseAssetName(key, allowLocales: this.TryLocalizeKeys).BaseName; + foreach (string key in this.Cache.Keys) + yield return new(key, this.Cache[key]); + } - // check if asset should be removed - bool remove = removeAssets.ContainsKey(baseAssetName); - if (!remove && predicate(baseAssetName, asset.GetType())) - { - removeAssets[baseAssetName] = asset; - remove = true; - } - return remove; - }, dispose); + /// + public bool InvalidateCache(IAssetName assetName, bool dispose = false) + { + if (!this.Cache.ContainsKey(assetName.Name)) + return false; - return removeAssets; + // remove from cache + this.Cache.Remove(assetName.Name, dispose); + return true; } /// diff --git a/src/SMAPI/Framework/ContentManagers/IContentManager.cs b/src/SMAPI/Framework/ContentManagers/IContentManager.cs index ac67cad5..f2e3b9f0 100644 --- a/src/SMAPI/Framework/ContentManagers/IContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/IContentManager.cs @@ -32,7 +32,7 @@ namespace StardewModdingAPI.Framework.ContentManagers /// The expected asset type. /// The normalized asset name. bool DoesAssetExist(IAssetName assetName) - where T: notnull; + where T : notnull; /// Load an asset through the content pipeline, using a localized variant of the if available. /// The type of asset to load. @@ -65,10 +65,13 @@ namespace StardewModdingAPI.Framework.ContentManagers /// The asset path relative to the loader root directory, not including the .xnb extension. bool IsLoaded(IAssetName assetName); + /// Get all assets in the cache. + IEnumerable> GetCachedAssets(); + /// Purge matched assets from the cache. - /// Matches the asset keys to invalidate. + /// The asset name to dispose. /// Whether to dispose invalidated assets. This should only be true when they're being invalidated as part of a dispose, to avoid crashing the game. - /// Returns the invalidated asset names and instances. - IDictionary InvalidateCache(Func predicate, bool dispose = false); + /// Returns whether the asset was in the cache. + bool InvalidateCache(IAssetName assetName, bool dispose = false); } } -- cgit From 0a050622f6d2b535bed3ddf28de9ef861469b759 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 19 May 2022 21:04:32 -0400 Subject: tweak asset propagator to avoid assumption that assets must be reloaded --- src/SMAPI/Metadata/CoreAssetPropagator.cs | 396 ++++++++++++++---------------- 1 file changed, 188 insertions(+), 208 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 6cb5a73a..dabd802d 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -110,12 +110,12 @@ namespace StardewModdingAPI.Metadata { case AssetBucket.Sprite: if (!ignoreWorld) - this.ReloadNpcSprites(propagatedAssets); + this.UpdateNpcSprites(propagatedAssets); break; case AssetBucket.Portrait: if (!ignoreWorld) - this.ReloadNpcPortraits(propagatedAssets); + this.UpdateNpcPortraits(propagatedAssets); break; default: @@ -198,7 +198,7 @@ namespace StardewModdingAPI.Metadata } var oldWarps = GetWarpSet(location); - this.ReloadMap(info); + this.UpdateMap(info); var newWarps = GetWarpSet(location); changedWarps = changedWarps || oldWarps.Count != newWarps.Count || oldWarps.Any(p => !newWarps.Contains(p)); @@ -219,7 +219,7 @@ namespace StardewModdingAPI.Metadata ** Animals ****/ case "animals/horse": - return !ignoreWorld && this.ReloadPetOrHorseSprites(content, assetName); + return !ignoreWorld && this.UpdatePetOrHorseSprites(content, assetName); /**** ** Buildings @@ -249,7 +249,7 @@ namespace StardewModdingAPI.Metadata case "characters/farmer/farmer_base_bald": case "characters/farmer/farmer_girl_base": case "characters/farmer/farmer_girl_base_bald": - return !ignoreWorld && this.ReloadPlayerSprites(assetName); + return !ignoreWorld && this.UpdatePlayerSprites(assetName); case "characters/farmer/hairstyles": // Game1.LoadContent FarmerRenderer.hairStylesTexture = this.LoadTexture(key); @@ -301,10 +301,10 @@ namespace StardewModdingAPI.Metadata return true; case "data/farmanimals": // FarmAnimal constructor - return !ignoreWorld && this.ReloadFarmAnimalData(); + return !ignoreWorld && this.UpdateFarmAnimalData(); case "data/hairdata": // Farmer.GetHairStyleMetadataFile - return this.ReloadHairData(); + return this.UpdateHairData(); case "data/movies": // MovieTheater.GetMovieData case "data/moviesreactions": // MovieTheater.GetMovieReactions @@ -312,7 +312,7 @@ namespace StardewModdingAPI.Metadata return true; case "data/npcdispositions": // NPC constructor - return !ignoreWorld && this.ReloadNpcDispositions(content, assetName); + return !ignoreWorld && this.UpdateNpcDispositions(content, assetName); case "data/npcgifttastes": // Game1.LoadContent Game1.NPCGiftTastes = content.Load>(key); @@ -392,7 +392,7 @@ namespace StardewModdingAPI.Metadata } if (!ignoreWorld) - this.ReloadDoorSprites(content, assetName); + this.UpdateDoorSprites(content, assetName); return true; case "loosesprites/cursors2": // Game1.LoadContent @@ -424,7 +424,7 @@ namespace StardewModdingAPI.Metadata return true; case "loosesprites/suspensionbridge": // SuspensionBridge constructor - return !ignoreWorld && this.ReloadSuspensionBridges(content, assetName); + return !ignoreWorld && this.UpdateSuspensionBridges(content, assetName); /**** ** Content\Maps @@ -455,13 +455,13 @@ namespace StardewModdingAPI.Metadata return false; case "minigames/titlebuttons": // TitleMenu - return this.ReloadTitleButtons(content, assetName); + return this.UpdateTitleButtons(content, assetName); /**** ** Content\Strings ****/ case "strings/stringsfromcsfiles": - return this.ReloadStringsFromCsFiles(content); + return this.UpdateStringsFromCsFiles(content); /**** ** Content\TileSheets @@ -479,14 +479,14 @@ namespace StardewModdingAPI.Metadata return true; case "tilesheets/chairtiles": // Game1.LoadContent - return this.ReloadChairTiles(content, assetName, ignoreWorld); + return this.UpdateChairTiles(content, assetName, ignoreWorld); case "tilesheets/craftables": // Game1.LoadContent Game1.bigCraftableSpriteSheet = content.Load(key); return true; case "tilesheets/critters": // Critter constructor - return !ignoreWorld && this.ReloadCritterTextures(content, assetName) > 0; + return !ignoreWorld && this.UpdateCritterTextures(content, assetName); case "tilesheets/crops": // Game1.LoadContent Game1.cropSpriteSheet = content.Load(key); @@ -540,7 +540,7 @@ namespace StardewModdingAPI.Metadata return true; case "terrainfeatures/grass": // from Grass - return !ignoreWorld && this.ReloadGrassTextures(content, assetName); + return !ignoreWorld && this.UpdateGrassTextures(content, assetName); case "terrainfeatures/hoedirt": // from HoeDirt HoeDirt.lightTexture = content.Load(key); @@ -555,27 +555,27 @@ namespace StardewModdingAPI.Metadata return true; case "terrainfeatures/mushroom_tree": // from Tree - return !ignoreWorld && this.ReloadTreeTextures(content, assetName, Tree.mushroomTree); + return !ignoreWorld && this.UpdateTreeTextures(content, assetName, Tree.mushroomTree); case "terrainfeatures/tree_palm": // from Tree - return !ignoreWorld && this.ReloadTreeTextures(content, assetName, Tree.palmTree); + return !ignoreWorld && this.UpdateTreeTextures(content, assetName, Tree.palmTree); case "terrainfeatures/tree1_fall": // from Tree case "terrainfeatures/tree1_spring": // from Tree case "terrainfeatures/tree1_summer": // from Tree case "terrainfeatures/tree1_winter": // from Tree - return !ignoreWorld && this.ReloadTreeTextures(content, assetName, Tree.bushyTree); + return !ignoreWorld && this.UpdateTreeTextures(content, assetName, Tree.bushyTree); case "terrainfeatures/tree2_fall": // from Tree case "terrainfeatures/tree2_spring": // from Tree case "terrainfeatures/tree2_summer": // from Tree case "terrainfeatures/tree2_winter": // from Tree - return !ignoreWorld && this.ReloadTreeTextures(content, assetName, Tree.leafyTree); + return !ignoreWorld && this.UpdateTreeTextures(content, assetName, Tree.leafyTree); case "terrainfeatures/tree3_fall": // from Tree case "terrainfeatures/tree3_spring": // from Tree case "terrainfeatures/tree3_winter": // from Tree - return !ignoreWorld && this.ReloadTreeTextures(content, assetName, Tree.pineTree); + return !ignoreWorld && this.UpdateTreeTextures(content, assetName, Tree.pineTree); } /**** @@ -585,24 +585,24 @@ namespace StardewModdingAPI.Metadata { // dynamic textures if (assetName.StartsWith("animals/cat")) - return this.ReloadPetOrHorseSprites(content, assetName); + return this.UpdatePetOrHorseSprites(content, assetName); if (assetName.StartsWith("animals/dog")) - return this.ReloadPetOrHorseSprites(content, assetName); + return this.UpdatePetOrHorseSprites(content, assetName); if (assetName.IsDirectlyUnderPath("Animals")) - return this.ReloadFarmAnimalSprites(content, assetName); + return this.UpdateFarmAnimalSprites(content, assetName); if (assetName.IsDirectlyUnderPath("Buildings")) - return this.ReloadBuildings(assetName); + return this.UpdateBuildings(assetName); if (assetName.StartsWith("LooseSprites/Fence")) - return this.ReloadFenceTextures(assetName); + return this.UpdateFenceTextures(assetName); // dynamic data if (assetName.IsDirectlyUnderPath("Characters/Dialogue")) - return this.ReloadNpcDialogue(assetName); + return this.UpdateNpcDialogue(assetName); if (assetName.IsDirectlyUnderPath("Characters/schedules")) - return this.ReloadNpcSchedules(assetName); + return this.UpdateNpcSchedules(assetName); } return false; @@ -613,14 +613,14 @@ namespace StardewModdingAPI.Metadata ** Private methods *********/ /**** - ** Reload texture methods + ** Update texture methods ****/ - /// Reload buttons on the title screen. - /// The content manager through which to reload the asset. - /// The asset name to reload. - /// Returns whether any textures were reloaded. + /// Update buttons on the title screen. + /// The content manager through which to update the asset. + /// The asset name to update. + /// Returns whether any references were updated. /// Derived from the constructor and . - private bool ReloadTitleButtons(LocalizedContentManager content, IAssetName assetName) + private bool UpdateTitleButtons(LocalizedContentManager content, IAssetName assetName) { if (Game1.activeClickableMenu is TitleMenu titleMenu) { @@ -641,12 +641,12 @@ namespace StardewModdingAPI.Metadata return false; } - /// Reload the sprites for matching pets or horses. + /// Update the sprites for matching pets or horses. /// The animal type. - /// The content manager through which to reload the asset. - /// The asset name to reload. - /// Returns whether any textures were reloaded. - private bool ReloadPetOrHorseSprites(LocalizedContentManager content, IAssetName assetName) + /// The content manager through which to update the asset. + /// The asset name to update. + /// Returns whether any references were updated. + private bool UpdatePetOrHorseSprites(LocalizedContentManager content, IAssetName assetName) where TAnimal : NPC { // find matches @@ -664,12 +664,12 @@ namespace StardewModdingAPI.Metadata return true; } - /// Reload the sprites for matching farm animals. - /// The content manager through which to reload the asset. - /// The asset name to reload. - /// Returns whether any textures were reloaded. + /// Update the sprites for matching farm animals. + /// The content manager through which to update the asset. + /// The asset name to update. + /// Returns whether any references were updated. /// Derived from . - private bool ReloadFarmAnimalSprites(LocalizedContentManager content, IAssetName assetName) + private bool UpdateFarmAnimalSprites(LocalizedContentManager content, IAssetName assetName) { // find matches FarmAnimal[] animals = this.GetFarmAnimals().ToArray(); @@ -695,10 +695,10 @@ namespace StardewModdingAPI.Metadata return texture.IsValueCreated; } - /// Reload building textures. - /// The asset name to reload. - /// Returns whether any textures were reloaded. - private bool ReloadBuildings(IAssetName assetName) + /// Update building textures. + /// The asset name to update. + /// Returns whether any references were updated. + private bool UpdateBuildings(IAssetName assetName) { // get paint mask info const string paintMaskSuffix = "_PaintMask"; @@ -731,12 +731,12 @@ namespace StardewModdingAPI.Metadata return removedFromCache; } - /// Reload map seat textures. + /// Update map seat textures. /// The content manager through which to reload the asset. - /// The asset name to reload. + /// The asset name to update. /// Whether the in-game world is fully unloaded (e.g. on the title screen), so there's no need to propagate changes into the world. - /// Returns whether any textures were reloaded. - private bool ReloadChairTiles(LocalizedContentManager content, IAssetName assetName, bool ignoreWorld) + /// Returns whether any references were updated. + private bool UpdateChairTiles(LocalizedContentManager content, IAssetName assetName, bool ignoreWorld) { MapSeat.mapChairTexture = content.Load(assetName.BaseName); @@ -755,11 +755,11 @@ namespace StardewModdingAPI.Metadata return true; } - /// Reload critter textures. + /// Update critter textures. /// The content manager through which to reload the asset. - /// The asset name to reload. - /// Returns the number of reloaded assets. - private int ReloadCritterTextures(LocalizedContentManager content, IAssetName assetName) + /// The asset name to update. + /// Returns whether any references were updated. + private bool UpdateCritterTextures(LocalizedContentManager content, IAssetName assetName) { // get critters Critter[] critters = @@ -772,21 +772,21 @@ namespace StardewModdingAPI.Metadata ) .ToArray(); if (!critters.Any()) - return 0; + return false; // update sprites Texture2D texture = content.Load(assetName.BaseName); foreach (Critter entry in critters) entry.sprite.spriteTexture = texture; - return critters.Length; + return true; } - /// Reload the sprites for interior doors. + /// Update the sprites for interior doors. /// The content manager through which to reload the asset. - /// The asset name to reload. - /// Returns whether any doors were affected. - private bool ReloadDoorSprites(LocalizedContentManager content, IAssetName assetName) + /// The asset name to update. + /// Returns whether any references were updated. + private void UpdateDoorSprites(LocalizedContentManager content, IAssetName assetName) { Lazy texture = new Lazy(() => content.Load(assetName.BaseName)); @@ -806,29 +806,12 @@ namespace StardewModdingAPI.Metadata door.Sprite.texture = texture.Value; } } - - return texture.IsValueCreated; - } - - /// Reload the data for matching farm animals. - /// Returns whether any farm animals were affected. - /// Derived from the constructor. - private bool ReloadFarmAnimalData() - { - bool changed = false; - foreach (FarmAnimal animal in this.GetFarmAnimals()) - { - animal.reloadData(); - changed = true; - } - - return changed; } - /// Reload the sprites for a fence type. - /// The asset name to reload. - /// Returns whether any textures were reloaded. - private bool ReloadFenceTextures(IAssetName assetName) + /// Update the sprites for a fence type. + /// The asset name to update. + /// Returns whether any references were updated. + private bool UpdateFenceTextures(IAssetName assetName) { // get fence type (e.g. LooseSprites/Fence3 => 3) if (!int.TryParse(this.GetSegments(assetName.BaseName)[1].Substring("Fence".Length), out int fenceType)) @@ -852,11 +835,11 @@ namespace StardewModdingAPI.Metadata return true; } - /// Reload tree textures. + /// Update tree textures. /// The content manager through which to reload the asset. - /// The asset name to reload. - /// Returns whether any textures were reloaded. - private bool ReloadGrassTextures(LocalizedContentManager content, IAssetName assetName) + /// The asset name to update. + /// Returns whether any references were updated. + private bool UpdateGrassTextures(LocalizedContentManager content, IAssetName assetName) { Grass[] grasses = ( @@ -878,77 +861,9 @@ namespace StardewModdingAPI.Metadata return false; } - /// Reload hair style metadata. - /// Returns whether any assets were reloaded. - /// Derived from the and . - private bool ReloadHairData() - { - if (Farmer.hairStyleMetadataFile == null) - return false; - - Farmer.hairStyleMetadataFile = null; - Farmer.allHairStyleIndices = null; - Farmer.hairStyleMetadata.Clear(); - - return true; - } - - /// Reload the map for a location. - /// The location whose map to reload. - private void ReloadMap(LocationInfo locationInfo) - { - GameLocation location = locationInfo.Location; - Vector2? playerPos = Game1.player?.Position; - - // reload map - location.interiorDoors.Clear(); // prevent errors when doors try to update tiles which no longer exist - location.reloadMap(); - - // reload interior doors - location.interiorDoors.Clear(); - location.interiorDoors.ResetSharedState(); // load doors from map properties - location.interiorDoors.ResetLocalState(); // reapply door tiles - - // reapply map changes (after reloading doors so they apply theirs too) - location.MakeMapModifications(force: true); - - // update for changes - location.updateWarps(); - location.updateDoors(); - locationInfo.ParentBuilding?.updateInteriorWarps(); - - // reset player position - // The game may move the player as part of the map changes, even if they're not in that - // location. That's not needed in this case, and it can have weird effects like players - // warping onto the wrong tile (or even off-screen) if a patch changes the farmhouse - // map on location change. - if (playerPos.HasValue) - Game1.player!.Position = playerPos.Value; - } - - /// Reload the disposition data for matching NPCs. - /// The content manager through which to reload the asset. - /// The asset name to reload. - /// Returns whether any NPCs were affected. - private bool ReloadNpcDispositions(LocalizedContentManager content, IAssetName assetName) - { - IDictionary data = content.Load>(assetName.BaseName); - bool changed = false; - foreach (NPC npc in this.GetCharacters()) - { - if (npc.isVillager() && data.ContainsKey(npc.Name)) - { - npc.reloadData(); - changed = true; - } - } - - return changed; - } - - /// Reload the sprites for matching NPCs. - /// The asset keys which are being propagated. - private void ReloadNpcSprites(IDictionary propagated) + /// Update the sprites for matching NPCs. + /// The asset names being propagated. + private void UpdateNpcSprites(IDictionary propagated) { // get NPCs var characters = @@ -970,9 +885,9 @@ namespace StardewModdingAPI.Metadata } } - /// Reload the portraits for matching NPCs. - /// The asset keys which are being propagated. - private void ReloadNpcPortraits(IDictionary propagated) + /// Update the portraits for matching NPCs. + /// The asset names being propagated. + private void UpdateNpcPortraits(IDictionary propagated) { // get NPCs var characters = @@ -1009,9 +924,9 @@ namespace StardewModdingAPI.Metadata } } - /// Reload the sprites for matching players. - /// The asset name to reload. - private bool ReloadPlayerSprites(IAssetName assetName) + /// Update the sprites for matching players. + /// The asset name to update. + private bool UpdatePlayerSprites(IAssetName assetName) { Farmer[] players = ( @@ -1032,11 +947,11 @@ namespace StardewModdingAPI.Metadata return players.Any(); } - /// Reload suspension bridge textures. + /// Update suspension bridge textures. /// The content manager through which to reload the asset. - /// The asset name to reload. - /// Returns whether any textures were reloaded. - private bool ReloadSuspensionBridges(LocalizedContentManager content, IAssetName assetName) + /// The asset name to update. + /// Returns whether any references were updated. + private bool UpdateSuspensionBridges(LocalizedContentManager content, IAssetName assetName) { Lazy texture = new Lazy(() => content.Load(assetName.BaseName)); @@ -1060,12 +975,12 @@ namespace StardewModdingAPI.Metadata return texture.IsValueCreated; } - /// Reload tree textures. + /// Update tree textures. /// The content manager through which to reload the asset. - /// The asset name to reload. - /// The type to reload. - /// Returns whether any textures were reloaded. - private bool ReloadTreeTextures(LocalizedContentManager content, IAssetName assetName, int type) + /// The asset name to update. + /// The type to update. + /// Returns whether any references were updated. + private bool UpdateTreeTextures(LocalizedContentManager content, IAssetName assetName, int type) { Tree[] trees = this.GetLocations() .SelectMany(p => p.terrainFeatures.Values.OfType()) @@ -1084,12 +999,42 @@ namespace StardewModdingAPI.Metadata } /**** - ** Reload data methods + ** Update data methods ****/ - /// Reload the dialogue data for matching NPCs. - /// The asset name to reload. - /// Returns whether any assets were reloaded. - private bool ReloadNpcDialogue(IAssetName assetName) + /// Update the data for matching farm animals. + /// Returns whether any farm animals were updated. + /// Derived from the constructor. + private bool UpdateFarmAnimalData() + { + bool changed = false; + foreach (FarmAnimal animal in this.GetFarmAnimals()) + { + animal.reloadData(); + changed = true; + } + + return changed; + } + + /// Update hair style metadata. + /// Returns whether any data was updated. + /// Derived from the and . + private bool UpdateHairData() + { + if (Farmer.hairStyleMetadataFile == null) + return false; + + Farmer.hairStyleMetadataFile = null; + Farmer.allHairStyleIndices = null; + Farmer.hairStyleMetadata.Clear(); + + return true; + } + + /// Update the dialogue data for matching NPCs. + /// The asset name to update. + /// Returns whether any NPCs were updated. + private bool UpdateNpcDialogue(IAssetName assetName) { // get NPCs string name = Path.GetFileName(assetName.BaseName); @@ -1116,10 +1061,30 @@ namespace StardewModdingAPI.Metadata return true; } - /// Reload the schedules for matching NPCs. - /// The asset name to reload. - /// Returns whether any assets were reloaded. - private bool ReloadNpcSchedules(IAssetName assetName) + /// Update the disposition data for matching NPCs. + /// The content manager through which to reload the asset. + /// The asset name to update. + /// Returns whether any NPCs were updated. + private bool UpdateNpcDispositions(LocalizedContentManager content, IAssetName assetName) + { + IDictionary data = content.Load>(assetName.BaseName); + bool changed = false; + foreach (NPC npc in this.GetCharacters()) + { + if (npc.isVillager() && data.ContainsKey(npc.Name)) + { + npc.reloadData(); + changed = true; + } + } + + return changed; + } + + /// Update the schedules for matching NPCs. + /// The asset name to update. + /// Returns whether any NPCs were updated. + private bool UpdateNpcSchedules(IAssetName assetName) { // get NPCs string name = Path.GetFileName(assetName.BaseName); @@ -1150,11 +1115,11 @@ namespace StardewModdingAPI.Metadata return true; } - /// Reload cached translations from the Strings\StringsFromCSFiles asset. + /// Update cached translations from the Strings\StringsFromCSFiles asset. /// The content manager through which to reload the asset. - /// Returns whether any data was reloaded. + /// Returns whether any data was updated. /// Derived from the . - private bool ReloadStringsFromCsFiles(LocalizedContentManager content) + private bool UpdateStringsFromCsFiles(LocalizedContentManager content) { Game1.samBandName = content.LoadString("Strings/StringsFromCSFiles:Game1.cs.2156"); Game1.elliottBookName = content.LoadString("Strings/StringsFromCSFiles:Game1.cs.2157"); @@ -1171,6 +1136,42 @@ namespace StardewModdingAPI.Metadata return true; } + /**** + ** Update map methods + ****/ + /// Update the map for a location. + /// The location whose map to update. + private void UpdateMap(LocationInfo locationInfo) + { + GameLocation location = locationInfo.Location; + Vector2? playerPos = Game1.player?.Position; + + // reload map + location.interiorDoors.Clear(); // prevent errors when doors try to update tiles which no longer exist + location.reloadMap(); + + // reload interior doors + location.interiorDoors.Clear(); + location.interiorDoors.ResetSharedState(); // load doors from map properties + location.interiorDoors.ResetLocalState(); // reapply door tiles + + // reapply map changes (after reloading doors so they apply theirs too) + location.MakeMapModifications(force: true); + + // update for changes + location.updateWarps(); + location.updateDoors(); + locationInfo.ParentBuilding?.updateInteriorWarps(); + + // reset player position + // The game may move the player as part of the map changes, even if they're not in that + // location. That's not needed in this case, and it can have weird effects like players + // warping onto the wrong tile (or even off-screen) if a patch changes the farmhouse + // map on location change. + if (playerPos.HasValue) + Game1.player!.Position = playerPos.Value; + } + /**** ** Helpers ****/ @@ -1299,29 +1300,8 @@ namespace StardewModdingAPI.Metadata } /// Metadata about a location used in asset propagation. - private readonly struct LocationInfo - { - /********* - ** Accessors - *********/ - /// The location instance. - public GameLocation Location { get; } - - /// The building which contains the location, if any. - public Building? ParentBuilding { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The location instance. - /// The building which contains the location, if any. - public LocationInfo(GameLocation location, Building? parentBuilding) - { - this.Location = location; - this.ParentBuilding = parentBuilding; - } - } + /// The location instance. + /// The building which contains the location, if any. + private record LocationInfo(GameLocation Location, Building? ParentBuilding); } } -- cgit From 7332879351f396572abd1785abf5d7807ef97ca4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 19 May 2022 21:04:32 -0400 Subject: defer asset reload during propagation when possible --- docs/release-notes.md | 1 + src/SMAPI/Metadata/CoreAssetPropagator.cs | 128 ++++++++++++++++-------------- 2 files changed, 70 insertions(+), 59 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 1f394849..8dc53e19 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -3,6 +3,7 @@ # Release notes ## Upcoming release * For players: + * Improved performance when mods change some asset types (including NPC portraits/sprites). * Fixed CurseForge update checks for the new CurseForge API. ## 3.14.4 diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index dabd802d..ce9ba7a8 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -219,7 +219,7 @@ namespace StardewModdingAPI.Metadata ** Animals ****/ case "animals/horse": - return !ignoreWorld && this.UpdatePetOrHorseSprites(content, assetName); + return !ignoreWorld && this.UpdatePetOrHorseSprites(assetName); /**** ** Buildings @@ -486,7 +486,7 @@ namespace StardewModdingAPI.Metadata return true; case "tilesheets/critters": // Critter constructor - return !ignoreWorld && this.UpdateCritterTextures(content, assetName); + return !ignoreWorld && this.UpdateCritterTextures(assetName); case "tilesheets/crops": // Game1.LoadContent Game1.cropSpriteSheet = content.Load(key); @@ -555,27 +555,27 @@ namespace StardewModdingAPI.Metadata return true; case "terrainfeatures/mushroom_tree": // from Tree - return !ignoreWorld && this.UpdateTreeTextures(content, assetName, Tree.mushroomTree); + return !ignoreWorld && this.UpdateTreeTextures(Tree.mushroomTree); case "terrainfeatures/tree_palm": // from Tree - return !ignoreWorld && this.UpdateTreeTextures(content, assetName, Tree.palmTree); + return !ignoreWorld && this.UpdateTreeTextures(Tree.palmTree); case "terrainfeatures/tree1_fall": // from Tree case "terrainfeatures/tree1_spring": // from Tree case "terrainfeatures/tree1_summer": // from Tree case "terrainfeatures/tree1_winter": // from Tree - return !ignoreWorld && this.UpdateTreeTextures(content, assetName, Tree.bushyTree); + return !ignoreWorld && this.UpdateTreeTextures(Tree.bushyTree); case "terrainfeatures/tree2_fall": // from Tree case "terrainfeatures/tree2_spring": // from Tree case "terrainfeatures/tree2_summer": // from Tree case "terrainfeatures/tree2_winter": // from Tree - return !ignoreWorld && this.UpdateTreeTextures(content, assetName, Tree.leafyTree); + return !ignoreWorld && this.UpdateTreeTextures(Tree.leafyTree); case "terrainfeatures/tree3_fall": // from Tree case "terrainfeatures/tree3_spring": // from Tree case "terrainfeatures/tree3_winter": // from Tree - return !ignoreWorld && this.UpdateTreeTextures(content, assetName, Tree.pineTree); + return !ignoreWorld && this.UpdateTreeTextures(Tree.pineTree); } /**** @@ -585,11 +585,11 @@ namespace StardewModdingAPI.Metadata { // dynamic textures if (assetName.StartsWith("animals/cat")) - return this.UpdatePetOrHorseSprites(content, assetName); + return this.UpdatePetOrHorseSprites(assetName); if (assetName.StartsWith("animals/dog")) - return this.UpdatePetOrHorseSprites(content, assetName); + return this.UpdatePetOrHorseSprites(assetName); if (assetName.IsDirectlyUnderPath("Animals")) - return this.UpdateFarmAnimalSprites(content, assetName); + return this.UpdateFarmAnimalSprites(assetName); if (assetName.IsDirectlyUnderPath("Buildings")) return this.UpdateBuildings(assetName); @@ -643,33 +643,29 @@ namespace StardewModdingAPI.Metadata /// Update the sprites for matching pets or horses. /// The animal type. - /// The content manager through which to update the asset. /// The asset name to update. /// Returns whether any references were updated. - private bool UpdatePetOrHorseSprites(LocalizedContentManager content, IAssetName assetName) + private bool UpdatePetOrHorseSprites(IAssetName assetName) where TAnimal : NPC { // find matches TAnimal[] animals = this.GetCharacters() .OfType() - .Where(p => this.IsSameBaseName(assetName, p.Sprite?.Texture?.Name)) + .Where(p => this.IsSameBaseName(assetName, p.Sprite?.spriteTexture?.Name)) .ToArray(); - if (!animals.Any()) - return false; // update sprites - Texture2D texture = content.Load(assetName.BaseName); + bool changed = false; foreach (TAnimal animal in animals) - animal.Sprite.spriteTexture = texture; - return true; + changed |= this.MarkSpriteDirty(animal.Sprite); + return changed; } /// Update the sprites for matching farm animals. - /// The content manager through which to update the asset. /// The asset name to update. /// Returns whether any references were updated. /// Derived from . - private bool UpdateFarmAnimalSprites(LocalizedContentManager content, IAssetName assetName) + private bool UpdateFarmAnimalSprites(IAssetName assetName) { // find matches FarmAnimal[] animals = this.GetFarmAnimals().ToArray(); @@ -677,7 +673,7 @@ namespace StardewModdingAPI.Metadata return false; // update sprites - Lazy texture = new Lazy(() => content.Load(assetName.BaseName)); + bool changed = true; foreach (FarmAnimal animal in animals) { // get expected key @@ -690,9 +686,9 @@ namespace StardewModdingAPI.Metadata // reload asset if (this.IsSameBaseName(assetName, expectedKey)) - animal.Sprite.spriteTexture = texture.Value; + changed |= this.MarkSpriteDirty(animal.Sprite); } - return texture.IsValueCreated; + return changed; } /// Update building textures. @@ -707,7 +703,7 @@ namespace StardewModdingAPI.Metadata // get building type string type = Path.GetFileName(assetName.BaseName); if (isPaintMask) - type = type.Substring(0, type.Length - paintMaskSuffix.Length); + type = type[..^paintMaskSuffix.Length]; // get buildings Building[] buildings = this.GetLocations(buildingInteriors: false) @@ -747,7 +743,7 @@ namespace StardewModdingAPI.Metadata foreach (MapSeat seat in location.mapSeats.Where(p => p != null)) { if (this.IsSameBaseName(assetName, seat._loadedTextureFile)) - seat.overlayTexture = MapSeat.mapChairTexture; + seat._loadedTextureFile = null; } } } @@ -756,10 +752,9 @@ namespace StardewModdingAPI.Metadata } /// Update critter textures. - /// The content manager through which to reload the asset. /// The asset name to update. /// Returns whether any references were updated. - private bool UpdateCritterTextures(LocalizedContentManager content, IAssetName assetName) + private bool UpdateCritterTextures(IAssetName assetName) { // get critters Critter[] critters = @@ -767,19 +762,16 @@ namespace StardewModdingAPI.Metadata from location in this.GetLocations() where location.critters != null from Critter critter in location.critters - where this.IsSameBaseName(assetName, critter.sprite?.Texture?.Name) + where this.IsSameBaseName(assetName, critter.sprite?.spriteTexture?.Name) select critter ) .ToArray(); - if (!critters.Any()) - return false; // update sprites - Texture2D texture = content.Load(assetName.BaseName); + bool changed = false; foreach (Critter entry in critters) - entry.sprite.spriteTexture = texture; - - return true; + changed |= this.MarkSpriteDirty(entry.sprite); + return changed; } /// Update the sprites for interior doors. @@ -814,7 +806,7 @@ namespace StardewModdingAPI.Metadata private bool UpdateFenceTextures(IAssetName assetName) { // get fence type (e.g. LooseSprites/Fence3 => 3) - if (!int.TryParse(this.GetSegments(assetName.BaseName)[1].Substring("Fence".Length), out int fenceType)) + if (!int.TryParse(this.GetSegments(assetName.BaseName)[1]["Fence".Length..], out int fenceType)) return false; // get fences @@ -830,9 +822,16 @@ namespace StardewModdingAPI.Metadata .ToArray(); // update fence textures + bool changed = false; foreach (Fence fence in fences) - fence.fenceTexture = new Lazy(fence.loadFenceTexture); - return true; + { + if (fence.fenceTexture.IsValueCreated) + { + fence.fenceTexture = new Lazy(fence.loadFenceTexture); + changed = true; + } + } + return changed; } /// Update tree textures. @@ -850,15 +849,16 @@ namespace StardewModdingAPI.Metadata ) .ToArray(); - if (grasses.Any()) + bool changed = false; + foreach (Grass grass in grasses) { - Lazy texture = new Lazy(() => content.Load(assetName.BaseName)); - foreach (Grass grass in grasses) - grass.texture = texture; - return true; + if (grass.texture.IsValueCreated) + { + grass.texture = new Lazy(() => content.Load(assetName.BaseName)); + changed = true; + } } - - return false; + return changed; } /// Update the sprites for matching NPCs. @@ -869,19 +869,17 @@ namespace StardewModdingAPI.Metadata var characters = ( from npc in this.GetCharacters() - let key = this.ParseAssetNameOrNull(npc.Sprite?.Texture?.Name)?.GetBaseAssetName() + let key = this.ParseAssetNameOrNull(npc.Sprite?.spriteTexture?.Name)?.GetBaseAssetName() where key != null && propagated.ContainsKey(key) select new { Npc = npc, AssetName = key } ) .ToArray(); - if (!characters.Any()) - return; // update sprite foreach (var target in characters) { - target.Npc.Sprite.spriteTexture = this.LoadTexture(target.AssetName.BaseName); - propagated[target.AssetName] = true; + if (this.MarkSpriteDirty(target.Npc.Sprite)) + propagated[target.AssetName] = true; } } @@ -919,7 +917,7 @@ namespace StardewModdingAPI.Metadata // update portrait foreach (var target in characters) { - target.Npc.Portrait = this.LoadTexture(target.AssetName.BaseName); + target.Npc.resetPortrait(); propagated[target.AssetName] = true; } } @@ -976,26 +974,38 @@ namespace StardewModdingAPI.Metadata } /// Update tree textures. - /// The content manager through which to reload the asset. - /// The asset name to update. /// The type to update. /// Returns whether any references were updated. - private bool UpdateTreeTextures(LocalizedContentManager content, IAssetName assetName, int type) + private bool UpdateTreeTextures(int type) { Tree[] trees = this.GetLocations() .SelectMany(p => p.terrainFeatures.Values.OfType()) .Where(tree => tree.treeType.Value == type) .ToArray(); - if (trees.Any()) + bool changed = false; + foreach (Tree tree in trees) { - Lazy texture = new Lazy(() => content.Load(assetName.BaseName)); - foreach (Tree tree in trees) - tree.texture = texture; - return true; + if (tree.texture.IsValueCreated) + { + this.Reflection.GetMethod(tree, "resetTexture").Invoke(); + changed = true; + } } + return changed; + } - return false; + /// Mark an animated sprite's texture dirty, so it's reloaded next time it's rendered. + /// The animated sprite to change. + /// Returns whether the sprite was changed. + private bool MarkSpriteDirty(AnimatedSprite sprite) + { + if (sprite.loadedTexture is null && sprite.spriteTexture is null) + return false; + + sprite.loadedTexture = null; + sprite.spriteTexture = null; + return true; } /**** -- cgit From e6ef71bae149b0d94055141075a84783f91e7c52 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 20 May 2022 17:39:05 -0400 Subject: add tick cache to asset propagation --- .../Framework/Utilities/TickCacheDictionary.cs | 26 +++++ src/SMAPI/Metadata/CoreAssetPropagator.cs | 123 ++++++++++++++------- 2 files changed, 112 insertions(+), 37 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/Utilities/TickCacheDictionary.cs b/src/SMAPI/Framework/Utilities/TickCacheDictionary.cs index 20d206e2..7732ace8 100644 --- a/src/SMAPI/Framework/Utilities/TickCacheDictionary.cs +++ b/src/SMAPI/Framework/Utilities/TickCacheDictionary.cs @@ -48,4 +48,30 @@ namespace StardewModdingAPI.Framework.Utilities return this.Cache.Remove(cacheKey); } } + + /// An in-memory dictionary cache that stores data for the duration of a game update tick. + /// The dictionary key type. + internal class TickCacheDictionary : TickCacheDictionary + where TKey : notnull + { + /********* + ** Public methods + *********/ + /// Get a value from the cache, fetching it first if it's not cached yet. + /// The unique key for the cached value. + /// Get the latest data if it's not in the cache yet. + public TValue GetOrSet(TKey cacheKey, Func get) + { + object? value = base.GetOrSet(cacheKey, () => get()!); + + try + { + return (TValue)value; + } + catch (Exception ex) + { + throw new InvalidCastException($"Can't cast value of the '{cacheKey}' cache entry from {value?.GetType().FullName ?? "null"} to {typeof(TValue).FullName}.", ex); + } + } + } } diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index ce9ba7a8..8ed6b591 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -7,6 +7,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Framework.ContentManagers; using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Framework.Utilities; using StardewModdingAPI.Internal; using StardewModdingAPI.Toolkit.Utilities; using StardewValley; @@ -58,6 +59,9 @@ namespace StardewModdingAPI.Metadata Other }; + /// A cache of world data fetched for the current tick. + private readonly TickCacheDictionary WorldCache = new(); + /********* ** Public methods @@ -842,8 +846,7 @@ namespace StardewModdingAPI.Metadata { Grass[] grasses = ( - from location in this.GetLocations() - from grass in location.terrainFeatures.Values.OfType() + from grass in this.GetTerrainFeatures().OfType() where this.IsSameBaseName(assetName, grass.textureName()) select grass ) @@ -978,8 +981,9 @@ namespace StardewModdingAPI.Metadata /// Returns whether any references were updated. private bool UpdateTreeTextures(int type) { - Tree[] trees = this.GetLocations() - .SelectMany(p => p.terrainFeatures.Values.OfType()) + Tree[] trees = this + .GetTerrainFeatures() + .OfType() .Where(tree => tree.treeType.Value == type) .ToArray(); @@ -1188,63 +1192,108 @@ namespace StardewModdingAPI.Metadata /// Get all NPCs in the game (excluding farm animals). private IEnumerable GetCharacters() { - foreach (NPC character in this.GetLocations().SelectMany(p => p.characters)) - yield return character; + return this.WorldCache.GetOrSet( + nameof(this.GetCharacters), + () => + { + List characters = new(); - if (Game1.CurrentEvent?.actors != null) - { - foreach (NPC character in Game1.CurrentEvent.actors) - yield return character; - } + foreach (NPC character in this.GetLocations().SelectMany(p => p.characters)) + characters.Add(character); + + if (Game1.CurrentEvent?.actors != null) + { + foreach (NPC character in Game1.CurrentEvent.actors) + characters.Add(character); + } + + return characters; + } + ); } /// Get all farm animals in the game. private IEnumerable GetFarmAnimals() { - foreach (GameLocation location in this.GetLocations()) - { - if (location is Farm farm) + return this.WorldCache.GetOrSet( + nameof(this.GetFarmAnimals), + () => { - foreach (FarmAnimal animal in farm.animals.Values) - yield return animal; + List animals = new(); + + foreach (GameLocation location in this.GetLocations()) + { + if (location is Farm farm) + { + foreach (FarmAnimal animal in farm.animals.Values) + animals.Add(animal); + } + else if (location is AnimalHouse animalHouse) + { + foreach (FarmAnimal animal in animalHouse.animals.Values) + animals.Add(animal); + } + } + + return animals; } - else if (location is AnimalHouse animalHouse) - foreach (FarmAnimal animal in animalHouse.animals.Values) - yield return animal; - } + ); } /// Get all locations in the game. /// Whether to also get the interior locations for constructable buildings. private IEnumerable GetLocations(bool buildingInteriors = true) { - return this.GetLocationsWithInfo(buildingInteriors).Select(info => info.Location); + return this.WorldCache.GetOrSet( + $"{nameof(this.GetLocations)}_{buildingInteriors}", + () => this.GetLocationsWithInfo(buildingInteriors).Select(info => info.Location).ToArray() + ); } /// Get all locations in the game. /// Whether to also get the interior locations for constructable buildings. private IEnumerable GetLocationsWithInfo(bool buildingInteriors = true) { - // get available root locations - IEnumerable rootLocations = Game1.locations; - if (SaveGame.loaded?.locations != null) - rootLocations = rootLocations.Concat(SaveGame.loaded.locations); + return this.WorldCache.GetOrSet( + $"{nameof(this.GetLocationsWithInfo)}_{buildingInteriors}", + () => + { + List locations = new(); - // yield root + child locations - foreach (GameLocation location in rootLocations) - { - yield return new LocationInfo(location, null); + // get root locations + foreach (GameLocation location in Game1.locations) + locations.Add(new LocationInfo(location, null)); + if (SaveGame.loaded?.locations != null) + { + foreach (GameLocation location in SaveGame.loaded.locations) + locations.Add(new LocationInfo(location, null)); + } - if (buildingInteriors && location is BuildableGameLocation buildableLocation) - { - foreach (Building building in buildableLocation.buildings) + // get child locations + if (buildingInteriors) { - GameLocation? indoors = building.indoors.Value; - if (indoors != null) - yield return new LocationInfo(indoors, building); + foreach (BuildableGameLocation location in locations.Select(p => p.Location).OfType().ToArray()) + { + foreach (Building building in location.buildings) + { + GameLocation indoors = building.indoors.Value; + if (indoors is not null) + locations.Add(new LocationInfo(indoors, building)); + } + } } - } - } + + return locations; + }); + } + + /// Get all terrain features in the game. + private IEnumerable GetTerrainFeatures() + { + return this.WorldCache.GetOrSet( + $"{nameof(this.GetTerrainFeatures)}", + () => this.GetLocations().SelectMany(p => p.terrainFeatures.Values).ToArray() + ); } /// Get whether two asset names are equivalent if you ignore the locale code. -- cgit From 7e7ac459a54a607de5843be37ea4b222058d5d3e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 21 May 2022 18:06:23 -0400 Subject: fix error when mod localizes an unlocalizable asset and then stops doing so --- docs/release-notes.md | 1 + src/SMAPI/Framework/ContentCoordinator.cs | 12 ++++++++++++ 2 files changed, 13 insertions(+) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 8dc53e19..9de06f3f 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -5,6 +5,7 @@ * For players: * Improved performance when mods change some asset types (including NPC portraits/sprites). * Fixed CurseForge update checks for the new CurseForge API. + * Fixed _could not find file_ error if a mod provides a localized version of a normally unlocalized asset and then stops providing it. ## 3.14.4 Released 15 May 2022 for Stardew Valley 1.5.6 or later. diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index dd3d2917..fc61b44b 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -407,6 +407,18 @@ namespace StardewModdingAPI.Framework } } + // forget localized flags + // A mod might provide a localized variant of a normally non-localized asset (like + // `Maps/MovieTheater.fr-FR`). When the asset is invalidated, we need to recheck + // whether the asset is localized in case it stops providing it. + foreach (IAssetName assetName in invalidatedAssets.Keys) + { + LocalizedContentManager.localizedAssetNames.Remove(assetName.Name); + + if (LocalizedContentManager.localizedAssetNames.TryGetValue(assetName.BaseName, out string? targetForBaseKey) && targetForBaseKey == assetName.Name) + LocalizedContentManager.localizedAssetNames.Remove(assetName.BaseName); + } + // special case: maps may be loaded through a temporary content manager that's removed while the map is still in use. // This notably affects the town and farmhouse maps. if (Game1.locations != null) -- cgit From 336cc1cc0f250c96ee23d45e1e08569b67a2e562 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 22 May 2022 14:38:33 -0400 Subject: prepare for release --- build/common.targets | 2 +- docs/release-notes.md | 6 ++++-- 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, 12 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/build/common.targets b/build/common.targets index 76343b8d..7b59e4ae 100644 --- a/build/common.targets +++ b/build/common.targets @@ -1,7 +1,7 @@ - 3.14.4 + 3.14.5 SMAPI latest $(AssemblySearchPaths);{GAC} diff --git a/docs/release-notes.md b/docs/release-notes.md index 9de06f3f..cd177f61 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,11 +1,13 @@ ← [README](README.md) # Release notes -## Upcoming release +## 3.14.5 +Released 22 May 2022 for Stardew Valley 1.5.6 or later. + * For players: * Improved performance when mods change some asset types (including NPC portraits/sprites). - * Fixed CurseForge update checks for the new CurseForge API. * Fixed _could not find file_ error if a mod provides a localized version of a normally unlocalized asset and then stops providing it. + * Fixed CurseForge update checks for the new CurseForge API. ## 3.14.4 Released 15 May 2022 for Stardew Valley 1.5.6 or later. diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index 78d26802..7b403a75 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.4", + "Version": "3.14.5", "Description": "Adds SMAPI console commands that let you manipulate the game.", "UniqueID": "SMAPI.ConsoleCommands", "EntryDll": "ConsoleCommands.dll", - "MinimumApiVersion": "3.14.4" + "MinimumApiVersion": "3.14.5" } diff --git a/src/SMAPI.Mods.ErrorHandler/manifest.json b/src/SMAPI.Mods.ErrorHandler/manifest.json index be1030f4..2ac959bb 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.4", + "Version": "3.14.5", "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.4" + "MinimumApiVersion": "3.14.5" } diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json index 8a50162e..707b6d8a 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.4", + "Version": "3.14.5", "Description": "Automatically backs up all your saves once per day into its folder.", "UniqueID": "SMAPI.SaveBackup", "EntryDll": "SaveBackup.dll", - "MinimumApiVersion": "3.14.4" + "MinimumApiVersion": "3.14.5" } diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 357b8db8..b2916a8d 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.4"; + internal static string RawApiVersion = "3.14.5"; } /// Contains SMAPI's constants and assumptions. -- cgit