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); + // 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[] invalidatedKeys = removedAssets.Keys.ToArray(); - string[] propagatedKeys = propagated.Where(p => p.Value).Select(p => p.Key).ToArray(); + // 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)); - this.Monitor.Log($"Invalidated {invalidatedKeys.Length} asset names ({FormatKeyList(invalidatedKeys)}); propagated {propagatedKeys.Length} core assets ({FormatKeyList(propagatedKeys)})."); + 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; } }