summaryrefslogtreecommitdiff
path: root/src/SMAPI/Metadata/CoreAssetPropagator.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/SMAPI/Metadata/CoreAssetPropagator.cs')
-rw-r--r--src/SMAPI/Metadata/CoreAssetPropagator.cs175
1 files changed, 123 insertions, 52 deletions
diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs
index 8b591bc1..52da3946 100644
--- a/src/SMAPI/Metadata/CoreAssetPropagator.cs
+++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs
@@ -79,8 +79,10 @@ namespace StardewModdingAPI.Metadata
/// <summary>Reload one of the game's core assets (if applicable).</summary>
/// <param name="assets">The asset keys and types to reload.</param>
- /// <returns>Returns a lookup of asset names to whether they've been propagated.</returns>
- public IDictionary<string, bool> Propagate(IDictionary<string, Type> assets)
+ /// <param name="ignoreWorld">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.</param>
+ /// <param name="propagatedAssets">A lookup of asset names to whether they've been propagated.</param>
+ /// <param name="updatedNpcWarps">Whether the NPC pathfinding cache was reloaded.</param>
+ public void Propagate(IDictionary<string, Type> assets, bool ignoreWorld, out IDictionary<string, bool> propagatedAssets, out bool updatedNpcWarps)
{
// group into optimized lists
var buckets = assets.GroupBy(p =>
@@ -95,26 +97,36 @@ namespace StardewModdingAPI.Metadata
});
// reload assets
- IDictionary<string, bool> 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:
- this.ReloadNpcSprites(bucket.Select(p => p.Key), propagated);
+ if (!ignoreWorld)
+ this.ReloadNpcSprites(bucket.Select(p => p.Key), propagatedAssets);
break;
case AssetBucket.Portrait:
- this.ReloadNpcPortraits(bucket.Select(p => p.Key), propagated);
+ if (!ignoreWorld)
+ this.ReloadNpcPortraits(bucket.Select(p => p.Key), propagatedAssets);
break;
default:
foreach (var entry in bucket)
- propagated[entry.Key] = this.PropagateOther(entry.Key, entry.Value);
+ {
+ 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();
}
@@ -124,19 +136,22 @@ namespace StardewModdingAPI.Metadata
/// <summary>Reload one of the game's core assets (if applicable).</summary>
/// <param name="key">The asset key to reload.</param>
/// <param name="type">The asset type to reload.</param>
+ /// <param name="ignoreWorld">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.</param>
+ /// <param name="changedWarps">Whether any map warps were changed as part of this propagation.</param>
/// <returns>Returns whether an asset was loaded. The return value may be true or false, or a non-null value for true.</returns>
[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, out bool changedWarps)
{
var content = this.MainContentManager;
key = this.AssertAndNormalizeAssetName(key);
+ changedWarps = false;
/****
** Special case: current map tilesheet
** 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 +166,30 @@ 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)
+ {
+ static ISet<string> GetWarpSet(GameLocation location)
+ {
+ return new HashSet<string>(
+ 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;
+ }
}
}
+
return anyChanged;
}
@@ -172,7 +203,7 @@ namespace StardewModdingAPI.Metadata
** Animals
****/
case "animals\\horse":
- return this.ReloadPetOrHorseSprites<Horse>(content, key);
+ return !ignoreWorld && this.ReloadPetOrHorseSprites<Horse>(content, key);
/****
** Buildings
@@ -197,7 +228,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 +301,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 +319,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<Dictionary<string, string>>(key);
@@ -366,6 +397,9 @@ namespace StardewModdingAPI.Metadata
foreach (ClickableTextureComponent button in new[] { menu.questButton, menu.zoomInButton, menu.zoomOutButton })
button.texture = Game1.mouseCursors;
}
+
+ if (!ignoreWorld)
+ this.ReloadDoorSprites(content, key);
return true;
case "loosesprites\\cursors2": // Game1.LoadContent
@@ -393,7 +427,7 @@ namespace StardewModdingAPI.Metadata
return true;
case "loosesprites\\suspensionbridge": // SuspensionBridge constructor
- return this.ReloadSuspensionBridges(content, key);
+ return !ignoreWorld && this.ReloadSuspensionBridges(content, key);
/****
** Content\Maps
@@ -452,14 +486,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<Texture2D>(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<Texture2D>(key);
@@ -513,7 +547,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<Texture2D>(key);
@@ -528,52 +562,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<Cat>(content, key);
- if (this.KeyStartsWith(key, "animals\\dog"))
- return this.ReloadPetOrHorseSprites<Dog>(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<Cat>(content, key);
+ if (this.KeyStartsWith(key, "animals\\dog"))
+ return this.ReloadPetOrHorseSprites<Dog>(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;
}
@@ -693,19 +730,23 @@ namespace StardewModdingAPI.Metadata
/// <summary>Reload map seat textures.</summary>
/// <param name="content">The content manager through which to reload the asset.</param>
/// <param name="key">The asset key to reload.</param>
+ /// <param name="ignoreWorld">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.</param>
/// <returns>Returns whether any textures were reloaded.</returns>
- private bool ReloadChairTiles(LocalizedContentManager content, string key)
+ private bool ReloadChairTiles(LocalizedContentManager content, string key, bool ignoreWorld)
{
MapSeat.mapChairTexture = content.Load<Texture2D>(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;
+ }
}
}
@@ -739,6 +780,36 @@ namespace StardewModdingAPI.Metadata
return critters.Length;
}
+ /// <summary>Reload the sprites for interior doors.</summary>
+ /// <param name="content">The content manager through which to reload the asset.</param>
+ /// <param name="key">The asset key to reload.</param>
+ /// <returns>Returns whether any doors were affected.</returns>
+ private bool ReloadDoorSprites(LocalizedContentManager content, string key)
+ {
+ Lazy<Texture2D> texture = new Lazy<Texture2D>(() => content.Load<Texture2D>(key));
+
+ foreach (GameLocation location in this.GetLocations())
+ {
+ IEnumerable<InteriorDoor> 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<string>(door.Sprite, "textureName").GetValue());
+ if (textureName != key)
+ continue;
+
+ door.Sprite.texture = texture.Value;
+ }
+ }
+
+ return texture.IsValueCreated;
+ }
+
/// <summary>Reload the data for matching farm animals.</summary>
/// <returns>Returns whether any farm animals were affected.</returns>
/// <remarks>Derived from the <see cref="FarmAnimal"/> constructor.</remarks>