From 3a9ea66a20136400d3268ea2e314eacd40d06231 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 26 Mar 2022 17:37:01 -0400 Subject: [PATCH] update asset propagation for new content API (#766) --- src/SMAPI/Framework/ContentCoordinator.cs | 5 +- .../ContentManagers/BaseContentManager.cs | 11 +-- .../ContentManagers/GameContentManager.cs | 29 ------ .../ContentManagers/IContentManager.cs | 3 - src/SMAPI/Metadata/CoreAssetPropagator.cs | 98 ++++++++++++------- 5 files changed, 66 insertions(+), 80 deletions(-) diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index a4d29068..ee8b6893 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -230,12 +230,9 @@ namespace StardewModdingAPI.Framework /// Perform any updates needed when the locale changes. public void OnLocaleChanged() { - // reload affected content + // reset baseline cache this.ContentManagerLock.InReadLock(() => { - foreach (IContentManager contentManager in this.ContentManagers) - contentManager.OnLocaleChanged(); - this.VanillaContentManager.Unload(); }); } diff --git a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs index b1ace259..1ca84792 100644 --- a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs @@ -137,16 +137,18 @@ namespace StardewModdingAPI.Framework.ContentManagers try { - this.LoadExact(localizedName, useCache: useCache); + T data = this.LoadExact(localizedName, useCache: useCache); LocalizedContentManager.localizedAssetNames[assetName.Name] = localizedName.Name; + return data; } catch (ContentLoadException) { localizedName = new AssetName(assetName.BaseName + "_international", null, null); try { - this.LoadExact(localizedName, useCache: useCache); + T data = this.LoadExact(localizedName, useCache: useCache); LocalizedContentManager.localizedAssetNames[assetName.Name] = localizedName.Name; + return data; } catch (ContentLoadException) { @@ -158,16 +160,13 @@ namespace StardewModdingAPI.Framework.ContentManagers // use cached key string rawName = LocalizedContentManager.localizedAssetNames[assetName.Name]; if (assetName.Name != rawName) - assetName = this.Coordinator.ParseAssetName(assetName.Name); + assetName = this.Coordinator.ParseAssetName(rawName); return this.LoadExact(assetName, useCache: useCache); } /// public abstract T LoadExact(IAssetName assetName, bool useCache); - /// - public virtual void OnLocaleChanged() { } - /// [SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local", Justification = "Parameter is only used for assertion checks by design.")] public string AssertAndNormalizeAssetName(string assetName) diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index 500c0191..0fcad30a 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -25,9 +25,6 @@ namespace StardewModdingAPI.Framework.ContentManagers /// The assets currently being intercepted by instances. This is used to prevent infinite loops when a loader loads a new asset. private readonly ContextHash AssetsBeingLoaded = new(); - /// Maps asset names to their localized form, like LooseSprites\Billboard => LooseSprites\Billboard.fr-FR (localized) or Maps\AnimalShop => Maps\AnimalShop (not localized). - private IDictionary LocalizedAssetNames => LocalizedContentManager.localizedAssetNames; - /// Whether the next load is the first for any game content manager. private static bool IsFirstLoad = true; @@ -139,32 +136,6 @@ namespace StardewModdingAPI.Framework.ContentManagers return data; } - /// - public override void OnLocaleChanged() - { - base.OnLocaleChanged(); - - // find assets for which a translatable version was loaded - HashSet removeAssetNames = new HashSet(StringComparer.OrdinalIgnoreCase); - foreach (string key in this.LocalizedAssetNames.Where(p => p.Key != p.Value).Select(p => p.Key)) - { - IAssetName assetName = this.Coordinator.ParseAssetName(key); - removeAssetNames.Add(assetName.BaseName); - } - - // invalidate translatable assets - string[] invalidated = this - .InvalidateCache((key, _) => - removeAssetNames.Contains(key) - || removeAssetNames.Contains(this.Coordinator.ParseAssetName(key).BaseName) - ) - .Select(p => p.Key) - .OrderBy(p => p, StringComparer.OrdinalIgnoreCase) - .ToArray(); - if (invalidated.Any()) - this.Monitor.Log($"Invalidated {invalidated.Length} asset names: {string.Join(", ", invalidated)} for locale change."); - } - /// public override LocalizedContentManager CreateTemporary() { diff --git a/src/SMAPI/Framework/ContentManagers/IContentManager.cs b/src/SMAPI/Framework/ContentManagers/IContentManager.cs index 4de9a8c3..774b20d9 100644 --- a/src/SMAPI/Framework/ContentManagers/IContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/IContentManager.cs @@ -65,8 +65,5 @@ namespace StardewModdingAPI.Framework.ContentManagers /// 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); - - /// Perform any cleanup needed when the locale changes. - void OnLocaleChanged(); } } diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index f645470e..832148aa 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -89,6 +89,14 @@ namespace StardewModdingAPI.Metadata /// Whether the NPC pathfinding cache was reloaded. public void Propagate(IDictionary assets, bool ignoreWorld, out IDictionary propagatedAssets, out bool updatedNpcWarps) { + // get base name lookup + propagatedAssets = assets + .Select(asset => asset.Key.GetBaseAssetName()) + .Distinct() + .ToDictionary(name => name, _ => false); + + this.Monitor.Log($"Propagating: {propagatedAssets.Keys.OrderBy(p => p.Name, StringComparer.OrdinalIgnoreCase)}", LogLevel.Alert); + // group into optimized lists var buckets = assets.GroupBy(p => { @@ -102,7 +110,6 @@ namespace StardewModdingAPI.Metadata }); // reload assets - propagatedAssets = assets.ToDictionary(p => p.Key, _ => false); updatedNpcWarps = false; foreach (var bucket in buckets) { @@ -110,12 +117,12 @@ namespace StardewModdingAPI.Metadata { case AssetBucket.Sprite: if (!ignoreWorld) - this.ReloadNpcSprites(bucket.Select(p => p.Key), propagatedAssets); + this.ReloadNpcSprites(propagatedAssets); break; case AssetBucket.Portrait: if (!ignoreWorld) - this.ReloadNpcPortraits(bucket.Select(p => p.Key), propagatedAssets); + this.ReloadNpcPortraits(propagatedAssets); break; default: @@ -158,7 +165,7 @@ namespace StardewModdingAPI.Metadata private bool PropagateOther(IAssetName assetName, Type type, bool ignoreWorld, out bool changedWarps) { var content = this.MainContentManager; - string key = assetName.Name; + string key = assetName.BaseName; changedWarps = false; /**** @@ -170,7 +177,7 @@ namespace StardewModdingAPI.Metadata { foreach (TileSheet tilesheet in Game1.currentLocation.map.TileSheets) { - if (assetName.IsEquivalentTo(tilesheet.ImageSource)) + if (this.IsSameBaseName(assetName, tilesheet.ImageSource)) Game1.mapDisplayDevice.LoadTileSheet(tilesheet); } } @@ -188,7 +195,7 @@ namespace StardewModdingAPI.Metadata { GameLocation location = info.Location; - if (assetName.IsEquivalentTo(location.mapPath.Value)) + if (this.IsSameBaseName(assetName, location.mapPath.Value)) { static ISet GetWarpSet(GameLocation location) { @@ -213,7 +220,7 @@ namespace StardewModdingAPI.Metadata /**** ** Propagate by key ****/ - switch (assetName.Name.ToLower().Replace("\\", "/")) // normalized key so we can compare statically + switch (assetName.BaseName.ToLower().Replace("\\", "/")) // normalized key so we can compare statically { /**** ** Animals @@ -624,7 +631,7 @@ namespace StardewModdingAPI.Metadata { if (Game1.activeClickableMenu is TitleMenu titleMenu) { - Texture2D texture = content.Load(assetName.Name); + Texture2D texture = content.Load(assetName.BaseName); titleMenu.titleButtonsTexture = texture; titleMenu.backButton.texture = texture; @@ -652,13 +659,13 @@ namespace StardewModdingAPI.Metadata // find matches TAnimal[] animals = this.GetCharacters() .OfType() - .Where(p => assetName.IsEquivalentTo(p.Sprite?.Texture?.Name)) + .Where(p => this.IsSameBaseName(assetName, p.Sprite?.Texture?.Name)) .ToArray(); if (!animals.Any()) return false; // update sprites - Texture2D texture = content.Load(assetName.Name); + Texture2D texture = content.Load(assetName.BaseName); foreach (TAnimal animal in animals) animal.Sprite.spriteTexture = texture; return true; @@ -677,7 +684,7 @@ namespace StardewModdingAPI.Metadata return false; // update sprites - Lazy texture = new Lazy(() => content.Load(assetName.Name)); + Lazy texture = new Lazy(() => content.Load(assetName.BaseName)); foreach (FarmAnimal animal in animals) { // get expected key @@ -689,7 +696,7 @@ namespace StardewModdingAPI.Metadata expectedKey = $"Animals/{expectedKey}"; // reload asset - if (assetName.IsEquivalentTo(expectedKey)) + if (this.IsSameBaseName(assetName, expectedKey)) animal.Sprite.spriteTexture = texture.Value; } return texture.IsValueCreated; @@ -705,7 +712,7 @@ namespace StardewModdingAPI.Metadata bool isPaintMask = assetName.BaseName.EndsWith(paintMaskSuffix, StringComparison.OrdinalIgnoreCase); // get building type - string type = Path.GetFileName(assetName.Name)!; + string type = Path.GetFileName(assetName.BaseName)!; if (isPaintMask) type = type.Substring(0, type.Length - paintMaskSuffix.Length); @@ -738,7 +745,7 @@ namespace StardewModdingAPI.Metadata /// Returns whether any textures were reloaded. private bool ReloadChairTiles(LocalizedContentManager content, IAssetName assetName, bool ignoreWorld) { - MapSeat.mapChairTexture = content.Load(assetName.Name); + MapSeat.mapChairTexture = content.Load(assetName.BaseName); if (!ignoreWorld) { @@ -746,7 +753,7 @@ namespace StardewModdingAPI.Metadata { foreach (MapSeat seat in location.mapSeats.Where(p => p != null)) { - if (assetName.IsEquivalentTo(seat._loadedTextureFile)) + if (this.IsSameBaseName(assetName, seat._loadedTextureFile)) seat.overlayTexture = MapSeat.mapChairTexture; } } @@ -767,7 +774,7 @@ namespace StardewModdingAPI.Metadata from location in this.GetLocations() where location.critters != null from Critter critter in location.critters - where assetName.IsEquivalentTo(critter.sprite?.Texture?.Name) + where this.IsSameBaseName(assetName, critter.sprite?.Texture?.Name) select critter ) .ToArray(); @@ -775,7 +782,7 @@ namespace StardewModdingAPI.Metadata return 0; // update sprites - Texture2D texture = content.Load(assetName.Name); + Texture2D texture = content.Load(assetName.BaseName); foreach (var entry in critters) entry.sprite.spriteTexture = texture; @@ -788,7 +795,7 @@ namespace StardewModdingAPI.Metadata /// Returns whether any doors were affected. private bool ReloadDoorSprites(LocalizedContentManager content, IAssetName assetName) { - Lazy texture = new Lazy(() => content.Load(assetName.Name)); + Lazy texture = new Lazy(() => content.Load(assetName.BaseName)); foreach (GameLocation location in this.GetLocations()) { @@ -802,7 +809,7 @@ namespace StardewModdingAPI.Metadata continue; string curKey = this.Reflection.GetField(door.Sprite, "textureName").GetValue(); - if (assetName.IsEquivalentTo(curKey)) + if (this.IsSameBaseName(assetName, curKey)) door.Sprite.texture = texture.Value; } } @@ -862,14 +869,14 @@ namespace StardewModdingAPI.Metadata ( from location in this.GetLocations() from grass in location.terrainFeatures.Values.OfType() - where assetName.IsEquivalentTo(grass.textureName()) + where this.IsSameBaseName(assetName, grass.textureName()) select grass ) .ToArray(); if (grasses.Any()) { - Lazy texture = new Lazy(() => content.Load(assetName.Name)); + Lazy texture = new Lazy(() => content.Load(assetName.BaseName)); foreach (Grass grass in grasses) grass.texture = texture; return true; @@ -935,7 +942,7 @@ namespace StardewModdingAPI.Metadata /// Returns whether any NPCs were affected. private bool ReloadNpcDispositions(LocalizedContentManager content, IAssetName assetName) { - IDictionary data = content.Load>(assetName.Name); + IDictionary data = content.Load>(assetName.BaseName); bool changed = false; foreach (NPC npc in this.GetCharacters()) { @@ -950,15 +957,14 @@ namespace StardewModdingAPI.Metadata } /// Reload the sprites for matching NPCs. - /// The asset keys to reload. - /// The asset keys which have been propagated. - private void ReloadNpcSprites(IEnumerable keys, IDictionary propagated) + /// The asset keys which are being propagated. + private void ReloadNpcSprites(IDictionary propagated) { // get NPCs var characters = ( from npc in this.GetCharacters() - let key = this.ParseAssetNameOrNull(npc.Sprite?.Texture?.Name) + let key = this.ParseAssetNameOrNull(npc.Sprite?.Texture?.Name)?.GetBaseAssetName() where key != null && propagated.ContainsKey(key) select new { Npc = npc, AssetName = key } ) @@ -969,15 +975,14 @@ namespace StardewModdingAPI.Metadata // update sprite foreach (var target in characters) { - target.Npc.Sprite.spriteTexture = this.LoadAndDisposeIfNeeded(target.Npc.Sprite.spriteTexture, target.AssetName.Name); + target.Npc.Sprite.spriteTexture = this.LoadAndDisposeIfNeeded(target.Npc.Sprite.spriteTexture, target.AssetName.BaseName); propagated[target.AssetName] = true; } } /// Reload the portraits for matching NPCs. - /// The asset key to reload. - /// The asset keys which have been propagated. - private void ReloadNpcPortraits(IEnumerable keys, IDictionary propagated) + /// The asset keys which are being propagated. + private void ReloadNpcPortraits(IDictionary propagated) { // get NPCs var characters = @@ -985,7 +990,7 @@ namespace StardewModdingAPI.Metadata from npc in this.GetCharacters() where npc.isVillager() - let key = this.ParseAssetNameOrNull(npc.Portrait?.Name) + let key = this.ParseAssetNameOrNull(npc.Portrait?.Name)?.GetBaseAssetName() where key != null && propagated.ContainsKey(key) select new { Npc = npc, AssetName = key } ) @@ -1005,7 +1010,7 @@ namespace StardewModdingAPI.Metadata // update portrait foreach (var target in characters) { - target.Npc.Portrait = this.LoadAndDisposeIfNeeded(target.Npc.Portrait, target.AssetName.Name); + target.Npc.Portrait = this.LoadAndDisposeIfNeeded(target.Npc.Portrait, target.AssetName.BaseName); propagated[target.AssetName] = true; } } @@ -1017,7 +1022,7 @@ namespace StardewModdingAPI.Metadata Farmer[] players = ( from player in Game1.getOnlineFarmers() - where assetName.IsEquivalentTo(player.getTexture()) + where this.IsSameBaseName(assetName, player.getTexture()) select player ) .ToArray(); @@ -1037,7 +1042,7 @@ namespace StardewModdingAPI.Metadata /// Returns whether any textures were reloaded. private bool ReloadSuspensionBridges(LocalizedContentManager content, IAssetName assetName) { - Lazy texture = new Lazy(() => content.Load(assetName.Name)); + Lazy texture = new Lazy(() => content.Load(assetName.BaseName)); foreach (GameLocation location in this.GetLocations(buildingInteriors: false)) { @@ -1068,7 +1073,7 @@ namespace StardewModdingAPI.Metadata if (trees.Any()) { - Lazy texture = new Lazy(() => content.Load(assetName.Name)); + Lazy texture = new Lazy(() => content.Load(assetName.BaseName)); foreach (Tree tree in trees) tree.texture = texture; return true; @@ -1086,7 +1091,7 @@ namespace StardewModdingAPI.Metadata private bool ReloadNpcDialogue(IAssetName assetName) { // get NPCs - string name = Path.GetFileName(assetName.Name); + string name = Path.GetFileName(assetName.BaseName); NPC[] villagers = this.GetCharacters().Where(npc => npc.Name == name && npc.isVillager()).ToArray(); if (!villagers.Any()) return false; @@ -1116,7 +1121,7 @@ namespace StardewModdingAPI.Metadata private bool ReloadNpcSchedules(IAssetName assetName) { // get NPCs - string name = Path.GetFileName(assetName.Name); + string name = Path.GetFileName(assetName.BaseName); NPC[] villagers = this.GetCharacters().Where(npc => npc.Name == name && npc.isVillager()).ToArray(); if (!villagers.Any()) return false; @@ -1230,6 +1235,23 @@ namespace StardewModdingAPI.Metadata } } + /// Get whether two asset names are equivalent if you ignore the locale code. + /// The first value to compare. + /// The second value to compare. + private bool IsSameBaseName(IAssetName left, string right) + { + IAssetName parsedB = this.ParseAssetNameOrNull(right); + return this.IsSameBaseName(left, parsedB); + } + + /// Get whether two asset names are equivalent if you ignore the locale code. + /// The first value to compare. + /// The second value to compare. + private bool IsSameBaseName(IAssetName left, IAssetName right) + { + return left.IsEquivalentTo(right.BaseName, useBaseName: true); + } + /// Normalize an asset key to match the cache key and assert that it's valid, but don't raise an error for null or empty values. /// The asset key to normalize. private IAssetName ParseAssetNameOrNull(string path) @@ -1281,7 +1303,7 @@ namespace StardewModdingAPI.Metadata BuildingPainter.paintMaskLookup = new Dictionary>>(BuildingPainter.paintMaskLookup, StringComparer.OrdinalIgnoreCase); // remove key from cache - return BuildingPainter.paintMaskLookup.Remove(assetName.Name); + return BuildingPainter.paintMaskLookup.Remove(assetName.BaseName); } /// Metadata about a location used in asset propagation.