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 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) (limited to 'src/SMAPI/Framework') 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())); } -- 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/SMAPI/Framework') 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/SMAPI/Framework') 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 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/SMAPI/Framework') 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/SMAPI/Framework') 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