summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/release-notes.md1
-rw-r--r--src/SMAPI/Framework/ContentCoordinator.cs9
-rw-r--r--src/SMAPI/Metadata/CoreAssetPropagator.cs263
3 files changed, 183 insertions, 90 deletions
diff --git a/docs/release-notes.md b/docs/release-notes.md
index 3718ac38..2b03579b 100644
--- a/docs/release-notes.md
+++ b/docs/release-notes.md
@@ -16,6 +16,7 @@ These changes have not been released yet.
* Fixed 'received message' logs shown in non-developer mode.
* Fixed some assets not updated when you switch language to English.
* Fixed lag in some cases due to incorrect asset caching when playing in non-English.
+ * Fixed lag when a mod invalidates many NPC portraits/sprites at once.
* For modders:
* Added support for content pack translations.
diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs
index 25eeb2ef..15f1c163 100644
--- a/src/SMAPI/Framework/ContentCoordinator.cs
+++ b/src/SMAPI/Framework/ContentCoordinator.cs
@@ -258,14 +258,7 @@ namespace StardewModdingAPI.Framework
}
// reload core game assets
- int reloaded = 0;
- foreach (var pair in removedAssetNames)
- {
- string key = pair.Key;
- Type type = pair.Value;
- if (this.CoreAssets.Propagate(this.MainContentManager, key, type)) // use an intercepted content manager
- reloaded++;
- }
+ int reloaded = this.CoreAssets.Propagate(this.MainContentManager, removedAssetNames); // use an intercepted content manager
// report result
if (removedAssetNames.Any())
diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs
index c4086712..dbb27b14 100644
--- a/src/SMAPI/Metadata/CoreAssetPropagator.cs
+++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs
@@ -34,6 +34,19 @@ namespace StardewModdingAPI.Metadata
/// <summary>Encapsulates monitoring and logging.</summary>
private readonly IMonitor Monitor;
+ /// <summary>Optimised bucket categories for batch reloading assets.</summary>
+ private enum AssetBucket
+ {
+ /// <summary>NPC overworld sprites.</summary>
+ Sprite,
+
+ /// <summary>Villager dialogue portraits.</summary>
+ Portrait,
+
+ /// <summary>Any other asset.</summary>
+ Other
+ };
+
/*********
** Public methods
@@ -51,15 +64,42 @@ namespace StardewModdingAPI.Metadata
/// <summary>Reload one of the game's core assets (if applicable).</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="type">The asset type to reload.</param>
- /// <returns>Returns whether an asset was reloaded.</returns>
- public bool Propagate(LocalizedContentManager content, string key, Type type)
+ /// <param name="assets">The asset keys and types to reload.</param>
+ /// <returns>Returns the number of reloaded assets.</returns>
+ public int Propagate(LocalizedContentManager content, IDictionary<string, Type> assets)
{
- object result = this.PropagateImpl(content, key, type);
- if (result is bool b)
- return b;
- return result != null;
+ // group into optimised lists
+ var buckets = assets.GroupBy(p =>
+ {
+ if (this.IsInFolder(p.Key, "Characters") || this.IsInFolder(p.Key, "Characters\\Monsters"))
+ return AssetBucket.Sprite;
+
+ if (this.IsInFolder(p.Key, "Portraits"))
+ return AssetBucket.Portrait;
+
+ return AssetBucket.Other;
+ });
+
+ // reload assets
+ int reloaded = 0;
+ foreach (var bucket in buckets)
+ {
+ switch (bucket.Key)
+ {
+ case AssetBucket.Sprite:
+ reloaded += this.ReloadNpcSprites(content, bucket.Select(p => p.Key));
+ break;
+
+ case AssetBucket.Portrait:
+ reloaded += this.ReloadNpcPortraits(content, bucket.Select(p => p.Key));
+ break;
+
+ default:
+ reloaded += bucket.Count(p => this.PropagateOther(content, p.Key, p.Value));
+ break;
+ }
+ }
+ return reloaded;
}
@@ -71,7 +111,7 @@ namespace StardewModdingAPI.Metadata
/// <param name="key">The asset key to reload.</param>
/// <param name="type">The asset type to reload.</param>
/// <returns>Returns whether an asset was loaded. The return value may be true or false, or a non-null value for true.</returns>
- private object PropagateImpl(LocalizedContentManager content, string key, Type type)
+ private bool PropagateOther(LocalizedContentManager content, string key, Type type)
{
key = this.GetNormalisedPath(key);
@@ -147,147 +187,185 @@ namespace StardewModdingAPI.Metadata
** Content\Characters\Farmer
****/
case "characters\\farmer\\accessories": // Game1.loadContent
- return FarmerRenderer.accessoriesTexture = content.Load<Texture2D>(key);
+ FarmerRenderer.accessoriesTexture = content.Load<Texture2D>(key);
+ return true;
case "characters\\farmer\\farmer_base": // Farmer
if (Game1.player == null || !Game1.player.IsMale)
return false;
- return Game1.player.FarmerRenderer = new FarmerRenderer(key, Game1.player);
+ Game1.player.FarmerRenderer = new FarmerRenderer(key, Game1.player);
+ return true;
case "characters\\farmer\\farmer_girl_base": // Farmer
if (Game1.player == null || Game1.player.IsMale)
return false;
- return Game1.player.FarmerRenderer = new FarmerRenderer(key, Game1.player);
+ Game1.player.FarmerRenderer = new FarmerRenderer(key, Game1.player);
+ return true;
case "characters\\farmer\\hairstyles": // Game1.loadContent
- return FarmerRenderer.hairStylesTexture = content.Load<Texture2D>(key);
+ FarmerRenderer.hairStylesTexture = content.Load<Texture2D>(key);
+ return true;
case "characters\\farmer\\hats": // Game1.loadContent
- return FarmerRenderer.hatsTexture = content.Load<Texture2D>(key);
+ FarmerRenderer.hatsTexture = content.Load<Texture2D>(key);
+ return true;
case "characters\\farmer\\shirts": // Game1.loadContent
- return FarmerRenderer.shirtsTexture = content.Load<Texture2D>(key);
+ FarmerRenderer.shirtsTexture = content.Load<Texture2D>(key);
+ return true;
/****
** Content\Data
****/
case "data\\achievements": // Game1.loadContent
- return Game1.achievements = content.Load<Dictionary<int, string>>(key);
+ Game1.achievements = content.Load<Dictionary<int, string>>(key);
+ return true;
case "data\\bigcraftablesinformation": // Game1.loadContent
- return Game1.bigCraftablesInformation = content.Load<Dictionary<int, string>>(key);
+ Game1.bigCraftablesInformation = content.Load<Dictionary<int, string>>(key);
+ return true;
case "data\\cookingrecipes": // CraftingRecipe.InitShared
- return CraftingRecipe.cookingRecipes = content.Load<Dictionary<string, string>>(key);
+ CraftingRecipe.cookingRecipes = content.Load<Dictionary<string, string>>(key);
+ return true;
case "data\\craftingrecipes": // CraftingRecipe.InitShared
- return CraftingRecipe.craftingRecipes = content.Load<Dictionary<string, string>>(key);
+ CraftingRecipe.craftingRecipes = content.Load<Dictionary<string, string>>(key);
+ return true;
case "data\\npcdispositions": // NPC constructor
return this.ReloadNpcDispositions(content, key);
case "data\\npcgifttastes": // Game1.loadContent
- return Game1.NPCGiftTastes = content.Load<Dictionary<string, string>>(key);
+ Game1.NPCGiftTastes = content.Load<Dictionary<string, string>>(key);
+ return true;
case "data\\objectinformation": // Game1.loadContent
- return Game1.objectInformation = content.Load<Dictionary<int, string>>(key);
+ Game1.objectInformation = content.Load<Dictionary<int, string>>(key);
+ return true;
/****
** Content\Fonts
****/
case "fonts\\spritefont1": // Game1.loadContent
- return Game1.dialogueFont = content.Load<SpriteFont>(key);
+ Game1.dialogueFont = content.Load<SpriteFont>(key);
+ return true;
case "fonts\\smallfont": // Game1.loadContent
- return Game1.smallFont = content.Load<SpriteFont>(key);
+ Game1.smallFont = content.Load<SpriteFont>(key);
+ return true;
case "fonts\\tinyfont": // Game1.loadContent
- return Game1.tinyFont = content.Load<SpriteFont>(key);
+ Game1.tinyFont = content.Load<SpriteFont>(key);
+ return true;
case "fonts\\tinyfontborder": // Game1.loadContent
- return Game1.tinyFontBorder = content.Load<SpriteFont>(key);
+ Game1.tinyFontBorder = content.Load<SpriteFont>(key);
+ return true;
/****
** Content\Lighting
****/
case "loosesprites\\lighting\\greenlight": // Game1.loadContent
- return Game1.cauldronLight = content.Load<Texture2D>(key);
+ Game1.cauldronLight = content.Load<Texture2D>(key);
+ return true;
case "loosesprites\\lighting\\indoorwindowlight": // Game1.loadContent
- return Game1.indoorWindowLight = content.Load<Texture2D>(key);
+ Game1.indoorWindowLight = content.Load<Texture2D>(key);
+ return true;
case "loosesprites\\lighting\\lantern": // Game1.loadContent
- return Game1.lantern = content.Load<Texture2D>(key);
+ Game1.lantern = content.Load<Texture2D>(key);
+ return true;
case "loosesprites\\lighting\\sconcelight": // Game1.loadContent
- return Game1.sconceLight = content.Load<Texture2D>(key);
+ Game1.sconceLight = content.Load<Texture2D>(key);
+ return true;
case "loosesprites\\lighting\\windowlight": // Game1.loadContent
- return Game1.windowLight = content.Load<Texture2D>(key);
+ Game1.windowLight = content.Load<Texture2D>(key);
+ return true;
/****
** Content\LooseSprites
****/
case "loosesprites\\controllermaps": // Game1.loadContent
- return Game1.controllerMaps = content.Load<Texture2D>(key);
+ Game1.controllerMaps = content.Load<Texture2D>(key);
+ return true;
case "loosesprites\\cursors": // Game1.loadContent
- return Game1.mouseCursors = content.Load<Texture2D>(key);
+ Game1.mouseCursors = content.Load<Texture2D>(key);
+ return true;
case "loosesprites\\daybg": // Game1.loadContent
- return Game1.daybg = content.Load<Texture2D>(key);
+ Game1.daybg = content.Load<Texture2D>(key);
+ return true;
case "loosesprites\\font_bold": // Game1.loadContent
- return SpriteText.spriteTexture = content.Load<Texture2D>(key);
+ SpriteText.spriteTexture = content.Load<Texture2D>(key);
+ return true;
case "loosesprites\\font_colored": // Game1.loadContent
- return SpriteText.coloredTexture = content.Load<Texture2D>(key);
+ SpriteText.coloredTexture = content.Load<Texture2D>(key);
+ return true;
case "loosesprites\\nightbg": // Game1.loadContent
- return Game1.nightbg = content.Load<Texture2D>(key);
+ Game1.nightbg = content.Load<Texture2D>(key);
+ return true;
case "loosesprites\\shadow": // Game1.loadContent
- return Game1.shadowTexture = content.Load<Texture2D>(key);
+ Game1.shadowTexture = content.Load<Texture2D>(key);
+ return true;
/****
** Content\Critters
****/
case "tilesheets\\crops": // Game1.loadContent
- return Game1.cropSpriteSheet = content.Load<Texture2D>(key);
+ Game1.cropSpriteSheet = content.Load<Texture2D>(key);
+ return true;
case "tilesheets\\debris": // Game1.loadContent
- return Game1.debrisSpriteSheet = content.Load<Texture2D>(key);
+ Game1.debrisSpriteSheet = content.Load<Texture2D>(key);
+ return true;
case "tilesheets\\emotes": // Game1.loadContent
- return Game1.emoteSpriteSheet = content.Load<Texture2D>(key);
+ Game1.emoteSpriteSheet = content.Load<Texture2D>(key);
+ return true;
case "tilesheets\\furniture": // Game1.loadContent
- return Furniture.furnitureTexture = content.Load<Texture2D>(key);
+ Furniture.furnitureTexture = content.Load<Texture2D>(key);
+ return true;
case "tilesheets\\projectiles": // Game1.loadContent
- return Projectile.projectileSheet = content.Load<Texture2D>(key);
+ Projectile.projectileSheet = content.Load<Texture2D>(key);
+ return true;
case "tilesheets\\rain": // Game1.loadContent
- return Game1.rainTexture = content.Load<Texture2D>(key);
+ Game1.rainTexture = content.Load<Texture2D>(key);
+ return true;
case "tilesheets\\tools": // Game1.ResetToolSpriteSheet
Game1.ResetToolSpriteSheet();
return true;
case "tilesheets\\weapons": // Game1.loadContent
- return Tool.weaponsTexture = content.Load<Texture2D>(key);
+ Tool.weaponsTexture = content.Load<Texture2D>(key);
+ return true;
/****
** Content\Maps
****/
case "maps\\menutiles": // Game1.loadContent
- return Game1.menuTexture = content.Load<Texture2D>(key);
+ Game1.menuTexture = content.Load<Texture2D>(key);
+ return true;
case "maps\\springobjects": // Game1.loadContent
- return Game1.objectSpriteSheet = content.Load<Texture2D>(key);
+ Game1.objectSpriteSheet = content.Load<Texture2D>(key);
+ return true;
case "maps\\walls_and_floors": // Wallpaper
- return Wallpaper.wallpaperTexture = content.Load<Texture2D>(key);
+ Wallpaper.wallpaperTexture = content.Load<Texture2D>(key);
+ return true;
/****
** Content\Minigames
@@ -315,35 +393,43 @@ namespace StardewModdingAPI.Metadata
** Content\TileSheets
****/
case "tilesheets\\animations": // Game1.loadContent
- return Game1.animations = content.Load<Texture2D>(key);
+ Game1.animations = content.Load<Texture2D>(key);
+ return true;
case "tilesheets\\buffsicons": // Game1.loadContent
- return Game1.buffsIcons = content.Load<Texture2D>(key);
+ Game1.buffsIcons = content.Load<Texture2D>(key);
+ return true;
case "tilesheets\\bushes": // new Bush()
reflection.GetField<Lazy<Texture2D>>(typeof(Bush), "texture").SetValue(new Lazy<Texture2D>(() => content.Load<Texture2D>(key)));
return true;
case "tilesheets\\craftables": // Game1.loadContent
- return Game1.bigCraftableSpriteSheet = content.Load<Texture2D>(key);
+ Game1.bigCraftableSpriteSheet = content.Load<Texture2D>(key);
+ return true;
case "tilesheets\\fruittrees": // FruitTree
- return FruitTree.texture = content.Load<Texture2D>(key);
+ FruitTree.texture = content.Load<Texture2D>(key);
+ return true;
/****
** Content\TerrainFeatures
****/
case "terrainfeatures\\flooring": // Flooring
- return Flooring.floorsTexture = content.Load<Texture2D>(key);
+ Flooring.floorsTexture = content.Load<Texture2D>(key);
+ return true;
case "terrainfeatures\\hoedirt": // from HoeDirt
- return HoeDirt.lightTexture = content.Load<Texture2D>(key);
+ HoeDirt.lightTexture = content.Load<Texture2D>(key);
+ return true;
case "terrainfeatures\\hoedirtdark": // from HoeDirt
- return HoeDirt.darkTexture = content.Load<Texture2D>(key);
+ HoeDirt.darkTexture = content.Load<Texture2D>(key);
+ return true;
case "terrainfeatures\\hoedirtsnow": // from HoeDirt
- return HoeDirt.snowTexture = content.Load<Texture2D>(key);
+ HoeDirt.snowTexture = content.Load<Texture2D>(key);
+ return true;
case "terrainfeatures\\mushroom_tree": // from Tree
return this.ReloadTreeTextures(content, key, Tree.mushroomTree);
@@ -376,15 +462,9 @@ namespace StardewModdingAPI.Metadata
if (this.IsInFolder(key, "Buildings"))
return this.ReloadBuildings(content, key);
- if (this.IsInFolder(key, "Characters") || this.IsInFolder(key, "Characters\\Monsters"))
- return this.ReloadNpcSprites(content, key);
-
if (this.KeyStartsWith(key, "LooseSprites\\Fence"))
return this.ReloadFenceTextures(key);
- if (this.IsInFolder(key, "Portraits"))
- return this.ReloadNpcPortraits(content, key);
-
// dynamic data
if (this.IsInFolder(key, "Characters\\Dialogue"))
return this.ReloadNpcDialogue(key);
@@ -536,46 +616,65 @@ namespace StardewModdingAPI.Metadata
/// <summary>Reload the sprites for matching NPCs.</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 textures were reloaded.</returns>
- private bool ReloadNpcSprites(LocalizedContentManager content, string key)
+ /// <param name="keys">The asset keys to reload.</param>
+ /// <returns>Returns the number of reloaded assets.</returns>
+ private int ReloadNpcSprites(LocalizedContentManager content, IEnumerable<string> keys)
{
// get NPCs
+ HashSet<string> lookup = new HashSet<string>(keys, StringComparer.InvariantCultureIgnoreCase);
NPC[] characters = this.GetCharacters()
- .Where(npc => npc.Sprite != null && this.GetNormalisedPath(npc.Sprite.textureName.Value) == key)
+ .Where(npc => npc.Sprite != null && lookup.Contains(this.GetNormalisedPath(npc.Sprite.textureName.Value)))
.ToArray();
if (!characters.Any())
- return false;
+ return 0;
- // update portrait
- Texture2D texture = content.Load<Texture2D>(key);
- foreach (NPC character in characters)
- this.SetSpriteTexture(character.Sprite, texture);
- return true;
+ // update sprite
+ int reloaded = 0;
+ foreach (NPC npc in characters)
+ {
+ this.SetSpriteTexture(npc.Sprite, content.Load<Texture2D>(npc.Sprite.textureName.Value));
+ reloaded++;
+ }
+
+ return reloaded;
}
/// <summary>Reload the portraits for matching NPCs.</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 textures were reloaded.</returns>
- private bool ReloadNpcPortraits(LocalizedContentManager content, string key)
+ /// <param name="keys">The asset key to reload.</param>
+ /// <returns>Returns the number of reloaded assets.</returns>
+ private int ReloadNpcPortraits(LocalizedContentManager content, IEnumerable<string> keys)
{
// get NPCs
- NPC[] villagers = this.GetCharacters()
- .Where(npc => npc.isVillager() && this.GetNormalisedPath($"Portraits\\{this.Reflection.GetMethod(npc, "getTextureName").Invoke<string>()}") == key)
+ HashSet<string> lookup = new HashSet<string>(keys, StringComparer.InvariantCultureIgnoreCase);
+ var villagers =
+ (
+ from npc in this.GetCharacters()
+ where npc.isVillager()
+ let textureKey = this.GetNormalisedPath($"Portraits\\{this.getTextureName(npc)}")
+ where lookup.Contains(textureKey)
+ select new { npc, textureKey }
+ )
.ToArray();
if (!villagers.Any())
- return false;
+ return 0;
// update portrait
- Texture2D texture = content.Load<Texture2D>(key);
- foreach (NPC villager in villagers)
+ int reloaded = 0;
+ foreach (var entry in villagers)
{
- villager.resetPortrait();
- villager.Portrait = texture;
+ entry.npc.resetPortrait();
+ entry.npc.Portrait = content.Load<Texture2D>(entry.textureKey);
+ reloaded++;
}
+ return reloaded;
+ }
- return true;
+ private string getTextureName(NPC npc)
+ {
+ string name = npc.Name;
+ string str = name == "Old Mariner" ? "Mariner" : (name == "Dwarf King" ? "DwarfKing" : (name == "Mister Qi" ? "MrQi" : (name == "???" ? "Monsters\\Shadow Guy" : name)));
+ return str;
}
/// <summary>Reload tree textures.</summary>