From 6805c90e2cdac734341b692298670b0beb50faa6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 14 Mar 2021 14:17:09 -0400 Subject: add asset propagation for interior door sprites --- src/SMAPI/Metadata/CoreAssetPropagator.cs | 32 +++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) (limited to 'src/SMAPI/Metadata/CoreAssetPropagator.cs') diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 8b591bc1..ae56dc9c 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -366,6 +366,8 @@ namespace StardewModdingAPI.Metadata foreach (ClickableTextureComponent button in new[] { menu.questButton, menu.zoomInButton, menu.zoomOutButton }) button.texture = Game1.mouseCursors; } + + this.ReloadDoorSprites(content, key); return true; case "loosesprites\\cursors2": // Game1.LoadContent @@ -739,6 +741,36 @@ namespace StardewModdingAPI.Metadata return critters.Length; } + /// Reload the sprites for interior doors. + /// The content manager through which to reload the asset. + /// The asset key to reload. + /// Returns whether any doors were affected. + private bool ReloadDoorSprites(LocalizedContentManager content, string key) + { + Lazy texture = new Lazy(() => content.Load(key)); + + foreach (GameLocation location in this.GetLocations()) + { + IEnumerable doors = location.interiorDoors?.Doors; + if (doors == null) + continue; + + foreach (InteriorDoor door in doors) + { + if (door?.Sprite == null) + continue; + + string textureName = this.NormalizeAssetNameIgnoringEmpty(this.Reflection.GetField(door.Sprite, "textureName").GetValue()); + if (textureName != key) + continue; + + 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. -- cgit From 749f0321f01b2f2ad865f31ab6f447c5a590fdd0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 16 Mar 2021 18:56:56 -0400 Subject: avoid asset propagation into the world if it's unloaded Propagating changes into world locations has no effect at this point (since they'll just be recreated when a save is loaded), and can noticeably impact performance. --- docs/release-notes.md | 1 + src/SMAPI/Context.cs | 3 + src/SMAPI/Framework/ContentCoordinator.cs | 13 +++- src/SMAPI/Metadata/CoreAssetPropagator.cs | 117 +++++++++++++++++------------- 4 files changed, 80 insertions(+), 54 deletions(-) (limited to 'src/SMAPI/Metadata/CoreAssetPropagator.cs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 9835dd64..0a4cd667 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -13,6 +13,7 @@ * For mod authors: * Added asset propagation for interior door sprites. + * Reduced performance impact of invalidating cached assets before a save is loaded. * Fixed assets changed by a mod not reapplied if playing in non-English, the changes are only applicable after the save is loaded, the player returns to title and reloads a save, and the game reloads the target asset before the save is loaded. ## 3.9.4 diff --git a/src/SMAPI/Context.cs b/src/SMAPI/Context.cs index b1b33cd6..5f70d0f7 100644 --- a/src/SMAPI/Context.cs +++ b/src/SMAPI/Context.cs @@ -38,6 +38,9 @@ namespace StardewModdingAPI set => Context.LoadStageForScreen.Value = value; } + /// Whether the in-game world is completely unloaded and not in the process of being loaded. The world may still exist in memory at this point, but should be ignored. + internal static bool IsWorldFullyUnloaded => Context.LoadStage == LoadStage.ReturningToTitle || Context.LoadStage == LoadStage.None; + /********* ** Accessors diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index 6d2ff441..5d4855ef 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -341,11 +341,16 @@ namespace StardewModdingAPI.Framework // reload core game assets if (removedAssets.Any()) { - IDictionary propagated = this.CoreAssets.Propagate(removedAssets.ToDictionary(p => p.Key, p => p.Value)); // use an intercepted content manager - this.Monitor.Log($"Invalidated {removedAssets.Count} asset names ({string.Join(", ", removedAssets.Keys.OrderBy(p => p, StringComparer.OrdinalIgnoreCase))}); propagated {propagated.Count(p => p.Value)} core assets.", LogLevel.Trace); + IDictionary propagated = this.CoreAssets.Propagate(removedAssets.ToDictionary(p => p.Key, p => p.Value), ignoreWorld: Context.IsWorldFullyUnloaded); + + string[] invalidatedKeys = removedAssets.Keys.ToArray(); + string[] propagatedKeys = propagated.Where(p => p.Value).Select(p => p.Key).ToArray(); + + string FormatKeyList(IEnumerable keys) => string.Join(", ", keys.OrderBy(p => p, StringComparer.OrdinalIgnoreCase)); + this.Monitor.Log($"Invalidated {invalidatedKeys.Length} asset names ({FormatKeyList(invalidatedKeys)}); propagated {propagatedKeys.Length} core assets ({FormatKeyList(propagatedKeys)})."); } else - this.Monitor.Log("Invalidated 0 cache entries.", LogLevel.Trace); + this.Monitor.Log("Invalidated 0 cache entries."); return removedAssets.Keys; } @@ -391,7 +396,7 @@ namespace StardewModdingAPI.Framework return; this.IsDisposed = true; - this.Monitor.Log("Disposing the content coordinator. Content managers will no longer be usable after this point.", LogLevel.Trace); + this.Monitor.Log("Disposing the content coordinator. Content managers will no longer be usable after this point."); foreach (IContentManager contentManager in this.ContentManagers) contentManager.Dispose(); this.ContentManagers.Clear(); diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index ae56dc9c..debbaffd 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -79,8 +79,9 @@ namespace StardewModdingAPI.Metadata /// Reload one of the game's core assets (if applicable). /// The asset keys and types to reload. + /// 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 a lookup of asset names to whether they've been propagated. - public IDictionary Propagate(IDictionary assets) + public IDictionary Propagate(IDictionary assets, bool ignoreWorld) { // group into optimized lists var buckets = assets.GroupBy(p => @@ -101,16 +102,18 @@ namespace StardewModdingAPI.Metadata switch (bucket.Key) { case AssetBucket.Sprite: - this.ReloadNpcSprites(bucket.Select(p => p.Key), propagated); + if (!ignoreWorld) + this.ReloadNpcSprites(bucket.Select(p => p.Key), propagated); break; case AssetBucket.Portrait: - this.ReloadNpcPortraits(bucket.Select(p => p.Key), propagated); + if (!ignoreWorld) + this.ReloadNpcPortraits(bucket.Select(p => p.Key), propagated); break; default: foreach (var entry in bucket) - propagated[entry.Key] = this.PropagateOther(entry.Key, entry.Value); + propagated[entry.Key] = this.PropagateOther(entry.Key, entry.Value, ignoreWorld); break; } } @@ -124,9 +127,10 @@ namespace StardewModdingAPI.Metadata /// Reload one of the game's core assets (if applicable). /// The asset key to reload. /// The asset type to reload. + /// 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 an asset was loaded. The return value may be true or false, or a non-null value for true. [SuppressMessage("ReSharper", "StringLiteralTypo", Justification = "These deliberately match the asset names.")] - private bool PropagateOther(string key, Type type) + private bool PropagateOther(string key, Type type, bool ignoreWorld) { var content = this.MainContentManager; key = this.AssertAndNormalizeAssetName(key); @@ -136,7 +140,7 @@ namespace StardewModdingAPI.Metadata ** We only need to do this for the current location, since tilesheets are reloaded when you enter a location. ** Just in case, we should still propagate by key even if a tilesheet is matched. ****/ - if (Game1.currentLocation?.map?.TileSheets != null) + if (!ignoreWorld && Game1.currentLocation?.map?.TileSheets != null) { foreach (TileSheet tilesheet in Game1.currentLocation.map.TileSheets) { @@ -151,14 +155,19 @@ namespace StardewModdingAPI.Metadata if (type == typeof(Map)) { bool anyChanged = false; - foreach (GameLocation location in this.GetLocations()) + + if (!ignoreWorld) { - if (!string.IsNullOrWhiteSpace(location.mapPath.Value) && this.NormalizeAssetNameIgnoringEmpty(location.mapPath.Value) == key) + foreach (GameLocation location in this.GetLocations()) { - this.ReloadMap(location); - anyChanged = true; + if (!string.IsNullOrWhiteSpace(location.mapPath.Value) && this.NormalizeAssetNameIgnoringEmpty(location.mapPath.Value) == key) + { + this.ReloadMap(location); + anyChanged = true; + } } } + return anyChanged; } @@ -172,7 +181,7 @@ namespace StardewModdingAPI.Metadata ** Animals ****/ case "animals\\horse": - return this.ReloadPetOrHorseSprites(content, key); + return !ignoreWorld && this.ReloadPetOrHorseSprites(content, key); /**** ** Buildings @@ -197,7 +206,7 @@ namespace StardewModdingAPI.Metadata case "characters\\farmer\\farmer_base_bald": case "characters\\farmer\\farmer_girl_base": case "characters\\farmer\\farmer_girl_base_bald": - return this.ReloadPlayerSprites(key); + return !ignoreWorld && this.ReloadPlayerSprites(key); case "characters\\farmer\\hairstyles": // Game1.LoadContent FarmerRenderer.hairStylesTexture = this.LoadAndDisposeIfNeeded(FarmerRenderer.hairStylesTexture, key); @@ -270,7 +279,7 @@ namespace StardewModdingAPI.Metadata return true; case "data\\farmanimals": // FarmAnimal constructor - return this.ReloadFarmAnimalData(); + return !ignoreWorld && this.ReloadFarmAnimalData(); case "data\\hairdata": // Farmer.GetHairStyleMetadataFile return this.ReloadHairData(); @@ -288,7 +297,7 @@ namespace StardewModdingAPI.Metadata return true; case "data\\npcdispositions": // NPC constructor - return this.ReloadNpcDispositions(content, key); + return !ignoreWorld && this.ReloadNpcDispositions(content, key); case "data\\npcgifttastes": // Game1.LoadContent Game1.NPCGiftTastes = content.Load>(key); @@ -367,7 +376,8 @@ namespace StardewModdingAPI.Metadata button.texture = Game1.mouseCursors; } - this.ReloadDoorSprites(content, key); + if (!ignoreWorld) + this.ReloadDoorSprites(content, key); return true; case "loosesprites\\cursors2": // Game1.LoadContent @@ -395,7 +405,7 @@ namespace StardewModdingAPI.Metadata return true; case "loosesprites\\suspensionbridge": // SuspensionBridge constructor - return this.ReloadSuspensionBridges(content, key); + return !ignoreWorld && this.ReloadSuspensionBridges(content, key); /**** ** Content\Maps @@ -454,14 +464,14 @@ namespace StardewModdingAPI.Metadata return true; case "tilesheets\\chairtiles": // Game1.LoadContent - return this.ReloadChairTiles(content, key); + return this.ReloadChairTiles(content, key, ignoreWorld); case "tilesheets\\craftables": // Game1.LoadContent Game1.bigCraftableSpriteSheet = content.Load(key); return true; case "tilesheets\\critters": // Critter constructor - return this.ReloadCritterTextures(content, key) > 0; + return !ignoreWorld && this.ReloadCritterTextures(content, key) > 0; case "tilesheets\\crops": // Game1.LoadContent Game1.cropSpriteSheet = content.Load(key); @@ -515,7 +525,7 @@ namespace StardewModdingAPI.Metadata return true; case "terrainfeatures\\grass": // from Grass - return this.ReloadGrassTextures(content, key); + return !ignoreWorld && this.ReloadGrassTextures(content, key); case "terrainfeatures\\hoedirt": // from HoeDirt HoeDirt.lightTexture = content.Load(key); @@ -530,52 +540,55 @@ namespace StardewModdingAPI.Metadata return true; case "terrainfeatures\\mushroom_tree": // from Tree - return this.ReloadTreeTextures(content, key, Tree.mushroomTree); + return !ignoreWorld && this.ReloadTreeTextures(content, key, Tree.mushroomTree); case "terrainfeatures\\tree_palm": // from Tree - return this.ReloadTreeTextures(content, key, Tree.palmTree); + return !ignoreWorld && this.ReloadTreeTextures(content, key, 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 this.ReloadTreeTextures(content, key, Tree.bushyTree); + return !ignoreWorld && this.ReloadTreeTextures(content, key, 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 this.ReloadTreeTextures(content, key, Tree.leafyTree); + return !ignoreWorld && this.ReloadTreeTextures(content, key, Tree.leafyTree); case "terrainfeatures\\tree3_fall": // from Tree case "terrainfeatures\\tree3_spring": // from Tree case "terrainfeatures\\tree3_winter": // from Tree - return this.ReloadTreeTextures(content, key, Tree.pineTree); + return !ignoreWorld && this.ReloadTreeTextures(content, key, Tree.pineTree); } /**** ** Dynamic assets ****/ - // dynamic textures - if (this.KeyStartsWith(key, "animals\\cat")) - return this.ReloadPetOrHorseSprites(content, key); - if (this.KeyStartsWith(key, "animals\\dog")) - return this.ReloadPetOrHorseSprites(content, key); - if (this.IsInFolder(key, "Animals")) - return this.ReloadFarmAnimalSprites(content, key); - - if (this.IsInFolder(key, "Buildings")) - return this.ReloadBuildings(content, key); - - if (this.KeyStartsWith(key, "LooseSprites\\Fence")) - return this.ReloadFenceTextures(key); - - // dynamic data - if (this.IsInFolder(key, "Characters\\Dialogue")) - return this.ReloadNpcDialogue(key); - - if (this.IsInFolder(key, "Characters\\schedules")) - return this.ReloadNpcSchedules(key); + if (!ignoreWorld) + { + // dynamic textures + if (this.KeyStartsWith(key, "animals\\cat")) + return this.ReloadPetOrHorseSprites(content, key); + if (this.KeyStartsWith(key, "animals\\dog")) + return this.ReloadPetOrHorseSprites(content, key); + if (this.IsInFolder(key, "Animals")) + return this.ReloadFarmAnimalSprites(content, key); + + if (this.IsInFolder(key, "Buildings")) + return this.ReloadBuildings(content, key); + + if (this.KeyStartsWith(key, "LooseSprites\\Fence")) + return this.ReloadFenceTextures(key); + + // dynamic data + if (this.IsInFolder(key, "Characters\\Dialogue")) + return this.ReloadNpcDialogue(key); + + if (this.IsInFolder(key, "Characters\\schedules")) + return this.ReloadNpcSchedules(key); + } return false; } @@ -695,19 +708,23 @@ namespace StardewModdingAPI.Metadata /// Reload map seat textures. /// The content manager through which to reload the asset. /// The asset key to reload. + /// 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, string key) + private bool ReloadChairTiles(LocalizedContentManager content, string key, bool ignoreWorld) { MapSeat.mapChairTexture = content.Load(key); - foreach (var location in this.GetLocations()) + if (!ignoreWorld) { - foreach (MapSeat seat in location.mapSeats.Where(p => p != null)) + foreach (var location in this.GetLocations()) { - string curKey = this.NormalizeAssetNameIgnoringEmpty(seat._loadedTextureFile); + foreach (MapSeat seat in location.mapSeats.Where(p => p != null)) + { + string curKey = this.NormalizeAssetNameIgnoringEmpty(seat._loadedTextureFile); - if (curKey == null || key.Equals(curKey, StringComparison.OrdinalIgnoreCase)) - seat.overlayTexture = MapSeat.mapChairTexture; + if (curKey == null || key.Equals(curKey, StringComparison.OrdinalIgnoreCase)) + seat.overlayTexture = MapSeat.mapChairTexture; + } } } -- cgit From c39b2b17663f79da92f3d0abe8c01ea73187cbab Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 19 Mar 2021 20:16:13 -0400 Subject: update NPC pathfinding cache when map warps change --- docs/release-notes.md | 8 ++++--- src/SMAPI/Framework/ContentCoordinator.cs | 31 ++++++++++++++++++++----- src/SMAPI/Framework/SCore.cs | 1 + src/SMAPI/Metadata/CoreAssetPropagator.cs | 38 ++++++++++++++++++++++++------- 4 files changed, 61 insertions(+), 17 deletions(-) (limited to 'src/SMAPI/Metadata/CoreAssetPropagator.cs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 2ebf061e..4eb196b6 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -13,9 +13,11 @@ * Disabled aggressive memory optimization (added in 3.9.2) by default. The option reduces errors for a subset of players who use certain mods, but may cause crashes for farmhands in multiplayer. You can edit `smapi-internal/config.json` to enable it if you experience frequent `OutOfMemoryException` errors. * For mod authors: - * Added asset propagation for interior door sprites. - * Reduced performance impact of invalidating cached assets before a save is loaded. - * Fixed assets changed by a mod not reapplied if playing in non-English, the changes are only applicable after the save is loaded, the player returns to title and reloads a save, and the game reloads the target asset before the save is loaded. + * Improved asset propagation: + * Added for interior door sprites. + * SMAPI now updates the NPC pathfinding cache when map warps are changed through the content API. + * Reduced performance impact of invalidating cached assets before a save is loaded. + * Fixed asset changes not reapplied in the edge case where you're playing in non-English, and the changes are only applied after the save is loaded, and the player returns to title and reloads a save, and the game reloads the target asset before the save is loaded. ## 3.9.4 Released 07 March 2021 for Stardew Valley 1.5.4 or later. diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index 5d4855ef..2920e670 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; +using System.Text; using System.Threading; using Microsoft.Xna.Framework.Content; using StardewModdingAPI.Framework.Content; @@ -341,13 +342,31 @@ namespace StardewModdingAPI.Framework // reload core game assets if (removedAssets.Any()) { - IDictionary propagated = this.CoreAssets.Propagate(removedAssets.ToDictionary(p => p.Key, p => p.Value), ignoreWorld: Context.IsWorldFullyUnloaded); - - string[] invalidatedKeys = removedAssets.Keys.ToArray(); - string[] propagatedKeys = propagated.Where(p => p.Value).Select(p => p.Key).ToArray(); + // propagate changes to the game + this.CoreAssets.Propagate( + assets: removedAssets.ToDictionary(p => p.Key, p => p.Value), + ignoreWorld: Context.IsWorldFullyUnloaded, + out IDictionary propagated, + out bool updatedNpcWarps + ); - string FormatKeyList(IEnumerable keys) => string.Join(", ", keys.OrderBy(p => p, StringComparer.OrdinalIgnoreCase)); - this.Monitor.Log($"Invalidated {invalidatedKeys.Length} asset names ({FormatKeyList(invalidatedKeys)}); propagated {propagatedKeys.Length} core assets ({FormatKeyList(propagatedKeys)})."); + // log summary + StringBuilder report = new StringBuilder(); + { + string[] invalidatedKeys = removedAssets.Keys.ToArray(); + string[] propagatedKeys = propagated.Where(p => p.Value).Select(p => p.Key).ToArray(); + + string FormatKeyList(IEnumerable keys) => string.Join(", ", keys.OrderBy(p => p, StringComparer.OrdinalIgnoreCase)); + + report.AppendLine($"Invalidated {invalidatedKeys.Length} asset names ({FormatKeyList(invalidatedKeys)})."); + report.AppendLine(propagated.Count > 0 + ? $"Propagated {propagatedKeys.Length} core assets ({FormatKeyList(propagatedKeys)})." + : "Propagated 0 core assets." + ); + if (updatedNpcWarps) + report.AppendLine("Updated NPC pathfinding cache."); + } + this.Monitor.Log(report.ToString().TrimEnd()); } else this.Monitor.Log("Invalidated 0 cache entries."); diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 5df4b61b..e98dc04c 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -482,6 +482,7 @@ namespace StardewModdingAPI.Framework + ")" ) ) + + "." ); // reload affected assets diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index debbaffd..52da3946 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -80,8 +80,9 @@ namespace StardewModdingAPI.Metadata /// Reload one of the game's core assets (if applicable). /// The asset keys and types to reload. /// 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 a lookup of asset names to whether they've been propagated. - public IDictionary Propagate(IDictionary assets, bool ignoreWorld) + /// A lookup of asset names to whether they've been propagated. + /// Whether the NPC pathfinding cache was reloaded. + public void Propagate(IDictionary assets, bool ignoreWorld, out IDictionary propagatedAssets, out bool updatedNpcWarps) { // group into optimized lists var buckets = assets.GroupBy(p => @@ -96,28 +97,36 @@ namespace StardewModdingAPI.Metadata }); // reload assets - IDictionary propagated = assets.ToDictionary(p => p.Key, _ => false, StringComparer.OrdinalIgnoreCase); + propagatedAssets = assets.ToDictionary(p => p.Key, _ => false, StringComparer.OrdinalIgnoreCase); + updatedNpcWarps = false; foreach (var bucket in buckets) { switch (bucket.Key) { case AssetBucket.Sprite: if (!ignoreWorld) - this.ReloadNpcSprites(bucket.Select(p => p.Key), propagated); + this.ReloadNpcSprites(bucket.Select(p => p.Key), propagatedAssets); break; case AssetBucket.Portrait: if (!ignoreWorld) - this.ReloadNpcPortraits(bucket.Select(p => p.Key), propagated); + this.ReloadNpcPortraits(bucket.Select(p => p.Key), propagatedAssets); break; default: foreach (var entry in bucket) - propagated[entry.Key] = this.PropagateOther(entry.Key, entry.Value, ignoreWorld); + { + bool changed = this.PropagateOther(entry.Key, entry.Value, ignoreWorld, out bool curChangedMapWarps); + propagatedAssets[entry.Key] = changed; + updatedNpcWarps = updatedNpcWarps || curChangedMapWarps; + } break; } } - return propagated; + + // reload NPC pathfinding cache if any map changed + if (updatedNpcWarps) + NPC.populateRoutesFromLocationToLocationList(); } @@ -128,12 +137,14 @@ namespace StardewModdingAPI.Metadata /// The asset key to reload. /// The asset type to reload. /// 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. + /// Whether any map warps were changed as part of this propagation. /// Returns whether an asset was loaded. The return value may be true or false, or a non-null value for true. [SuppressMessage("ReSharper", "StringLiteralTypo", Justification = "These deliberately match the asset names.")] - private bool PropagateOther(string key, Type type, bool ignoreWorld) + private bool PropagateOther(string key, Type type, bool ignoreWorld, out bool changedWarps) { var content = this.MainContentManager; key = this.AssertAndNormalizeAssetName(key); + changedWarps = false; /**** ** Special case: current map tilesheet @@ -162,7 +173,18 @@ namespace StardewModdingAPI.Metadata { if (!string.IsNullOrWhiteSpace(location.mapPath.Value) && this.NormalizeAssetNameIgnoringEmpty(location.mapPath.Value) == key) { + static ISet GetWarpSet(GameLocation location) + { + return new HashSet( + location.warps.Select(p => $"{p.X} {p.Y} {p.TargetName} {p.TargetX} {p.TargetY}") + ); + } + + var oldWarps = GetWarpSet(location); this.ReloadMap(location); + var newWarps = GetWarpSet(location); + + changedWarps = changedWarps || oldWarps.Count != newWarps.Count || oldWarps.Any(p => !newWarps.Contains(p)); anyChanged = true; } } -- cgit