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.
This commit is contained in:
Jesse Plamondon-Willard 2021-03-16 18:56:56 -04:00
parent 6805c90e2c
commit 749f0321f0
No known key found for this signature in database
GPG Key ID: CF8B1456B3E29F49
4 changed files with 76 additions and 50 deletions

View File

@ -13,6 +13,7 @@
* For mod authors: * For mod authors:
* Added asset propagation for interior door sprites. * 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. * 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 ## 3.9.4

View File

@ -38,6 +38,9 @@ namespace StardewModdingAPI
set => Context.LoadStageForScreen.Value = value; set => Context.LoadStageForScreen.Value = value;
} }
/// <summary>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.</summary>
internal static bool IsWorldFullyUnloaded => Context.LoadStage == LoadStage.ReturningToTitle || Context.LoadStage == LoadStage.None;
/********* /*********
** Accessors ** Accessors

View File

@ -341,11 +341,16 @@ namespace StardewModdingAPI.Framework
// reload core game assets // reload core game assets
if (removedAssets.Any()) if (removedAssets.Any())
{ {
IDictionary<string, bool> propagated = this.CoreAssets.Propagate(removedAssets.ToDictionary(p => p.Key, p => p.Value)); // use an intercepted content manager IDictionary<string, bool> propagated = this.CoreAssets.Propagate(removedAssets.ToDictionary(p => p.Key, p => p.Value), ignoreWorld: Context.IsWorldFullyUnloaded);
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);
string[] invalidatedKeys = removedAssets.Keys.ToArray();
string[] propagatedKeys = propagated.Where(p => p.Value).Select(p => p.Key).ToArray();
string FormatKeyList(IEnumerable<string> 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 else
this.Monitor.Log("Invalidated 0 cache entries.", LogLevel.Trace); this.Monitor.Log("Invalidated 0 cache entries.");
return removedAssets.Keys; return removedAssets.Keys;
} }
@ -391,7 +396,7 @@ namespace StardewModdingAPI.Framework
return; return;
this.IsDisposed = true; 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) foreach (IContentManager contentManager in this.ContentManagers)
contentManager.Dispose(); contentManager.Dispose();
this.ContentManagers.Clear(); this.ContentManagers.Clear();

View File

@ -79,8 +79,9 @@ namespace StardewModdingAPI.Metadata
/// <summary>Reload one of the game's core assets (if applicable).</summary> /// <summary>Reload one of the game's core assets (if applicable).</summary>
/// <param name="assets">The asset keys and types to reload.</param> /// <param name="assets">The asset keys and types 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 a lookup of asset names to whether they've been propagated.</returns> /// <returns>Returns a lookup of asset names to whether they've been propagated.</returns>
public IDictionary<string, bool> Propagate(IDictionary<string, Type> assets) public IDictionary<string, bool> Propagate(IDictionary<string, Type> assets, bool ignoreWorld)
{ {
// group into optimized lists // group into optimized lists
var buckets = assets.GroupBy(p => var buckets = assets.GroupBy(p =>
@ -101,16 +102,18 @@ namespace StardewModdingAPI.Metadata
switch (bucket.Key) switch (bucket.Key)
{ {
case AssetBucket.Sprite: case AssetBucket.Sprite:
this.ReloadNpcSprites(bucket.Select(p => p.Key), propagated); if (!ignoreWorld)
this.ReloadNpcSprites(bucket.Select(p => p.Key), propagated);
break; break;
case AssetBucket.Portrait: case AssetBucket.Portrait:
this.ReloadNpcPortraits(bucket.Select(p => p.Key), propagated); if (!ignoreWorld)
this.ReloadNpcPortraits(bucket.Select(p => p.Key), propagated);
break; break;
default: default:
foreach (var entry in bucket) 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; break;
} }
} }
@ -124,9 +127,10 @@ namespace StardewModdingAPI.Metadata
/// <summary>Reload one of the game's core assets (if applicable).</summary> /// <summary>Reload one of the game's core assets (if applicable).</summary>
/// <param name="key">The asset key to reload.</param> /// <param name="key">The asset key to reload.</param>
/// <param name="type">The asset type 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>
/// <returns>Returns whether an asset was loaded. The return value may be true or false, or a non-null value for true.</returns> /// <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.")] [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; var content = this.MainContentManager;
key = this.AssertAndNormalizeAssetName(key); 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. ** 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. ** 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) foreach (TileSheet tilesheet in Game1.currentLocation.map.TileSheets)
{ {
@ -151,14 +155,19 @@ namespace StardewModdingAPI.Metadata
if (type == typeof(Map)) if (type == typeof(Map))
{ {
bool anyChanged = false; 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); if (!string.IsNullOrWhiteSpace(location.mapPath.Value) && this.NormalizeAssetNameIgnoringEmpty(location.mapPath.Value) == key)
anyChanged = true; {
this.ReloadMap(location);
anyChanged = true;
}
} }
} }
return anyChanged; return anyChanged;
} }
@ -172,7 +181,7 @@ namespace StardewModdingAPI.Metadata
** Animals ** Animals
****/ ****/
case "animals\\horse": case "animals\\horse":
return this.ReloadPetOrHorseSprites<Horse>(content, key); return !ignoreWorld && this.ReloadPetOrHorseSprites<Horse>(content, key);
/**** /****
** Buildings ** Buildings
@ -197,7 +206,7 @@ namespace StardewModdingAPI.Metadata
case "characters\\farmer\\farmer_base_bald": case "characters\\farmer\\farmer_base_bald":
case "characters\\farmer\\farmer_girl_base": case "characters\\farmer\\farmer_girl_base":
case "characters\\farmer\\farmer_girl_base_bald": case "characters\\farmer\\farmer_girl_base_bald":
return this.ReloadPlayerSprites(key); return !ignoreWorld && this.ReloadPlayerSprites(key);
case "characters\\farmer\\hairstyles": // Game1.LoadContent case "characters\\farmer\\hairstyles": // Game1.LoadContent
FarmerRenderer.hairStylesTexture = this.LoadAndDisposeIfNeeded(FarmerRenderer.hairStylesTexture, key); FarmerRenderer.hairStylesTexture = this.LoadAndDisposeIfNeeded(FarmerRenderer.hairStylesTexture, key);
@ -270,7 +279,7 @@ namespace StardewModdingAPI.Metadata
return true; return true;
case "data\\farmanimals": // FarmAnimal constructor case "data\\farmanimals": // FarmAnimal constructor
return this.ReloadFarmAnimalData(); return !ignoreWorld && this.ReloadFarmAnimalData();
case "data\\hairdata": // Farmer.GetHairStyleMetadataFile case "data\\hairdata": // Farmer.GetHairStyleMetadataFile
return this.ReloadHairData(); return this.ReloadHairData();
@ -288,7 +297,7 @@ namespace StardewModdingAPI.Metadata
return true; return true;
case "data\\npcdispositions": // NPC constructor case "data\\npcdispositions": // NPC constructor
return this.ReloadNpcDispositions(content, key); return !ignoreWorld && this.ReloadNpcDispositions(content, key);
case "data\\npcgifttastes": // Game1.LoadContent case "data\\npcgifttastes": // Game1.LoadContent
Game1.NPCGiftTastes = content.Load<Dictionary<string, string>>(key); Game1.NPCGiftTastes = content.Load<Dictionary<string, string>>(key);
@ -367,7 +376,8 @@ namespace StardewModdingAPI.Metadata
button.texture = Game1.mouseCursors; button.texture = Game1.mouseCursors;
} }
this.ReloadDoorSprites(content, key); if (!ignoreWorld)
this.ReloadDoorSprites(content, key);
return true; return true;
case "loosesprites\\cursors2": // Game1.LoadContent case "loosesprites\\cursors2": // Game1.LoadContent
@ -395,7 +405,7 @@ namespace StardewModdingAPI.Metadata
return true; return true;
case "loosesprites\\suspensionbridge": // SuspensionBridge constructor case "loosesprites\\suspensionbridge": // SuspensionBridge constructor
return this.ReloadSuspensionBridges(content, key); return !ignoreWorld && this.ReloadSuspensionBridges(content, key);
/**** /****
** Content\Maps ** Content\Maps
@ -454,14 +464,14 @@ namespace StardewModdingAPI.Metadata
return true; return true;
case "tilesheets\\chairtiles": // Game1.LoadContent case "tilesheets\\chairtiles": // Game1.LoadContent
return this.ReloadChairTiles(content, key); return this.ReloadChairTiles(content, key, ignoreWorld);
case "tilesheets\\craftables": // Game1.LoadContent case "tilesheets\\craftables": // Game1.LoadContent
Game1.bigCraftableSpriteSheet = content.Load<Texture2D>(key); Game1.bigCraftableSpriteSheet = content.Load<Texture2D>(key);
return true; return true;
case "tilesheets\\critters": // Critter constructor case "tilesheets\\critters": // Critter constructor
return this.ReloadCritterTextures(content, key) > 0; return !ignoreWorld && this.ReloadCritterTextures(content, key) > 0;
case "tilesheets\\crops": // Game1.LoadContent case "tilesheets\\crops": // Game1.LoadContent
Game1.cropSpriteSheet = content.Load<Texture2D>(key); Game1.cropSpriteSheet = content.Load<Texture2D>(key);
@ -515,7 +525,7 @@ namespace StardewModdingAPI.Metadata
return true; return true;
case "terrainfeatures\\grass": // from Grass case "terrainfeatures\\grass": // from Grass
return this.ReloadGrassTextures(content, key); return !ignoreWorld && this.ReloadGrassTextures(content, key);
case "terrainfeatures\\hoedirt": // from HoeDirt case "terrainfeatures\\hoedirt": // from HoeDirt
HoeDirt.lightTexture = content.Load<Texture2D>(key); HoeDirt.lightTexture = content.Load<Texture2D>(key);
@ -530,52 +540,55 @@ namespace StardewModdingAPI.Metadata
return true; return true;
case "terrainfeatures\\mushroom_tree": // from Tree 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 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_fall": // from Tree
case "terrainfeatures\\tree1_spring": // from Tree case "terrainfeatures\\tree1_spring": // from Tree
case "terrainfeatures\\tree1_summer": // from Tree case "terrainfeatures\\tree1_summer": // from Tree
case "terrainfeatures\\tree1_winter": // 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_fall": // from Tree
case "terrainfeatures\\tree2_spring": // from Tree case "terrainfeatures\\tree2_spring": // from Tree
case "terrainfeatures\\tree2_summer": // from Tree case "terrainfeatures\\tree2_summer": // from Tree
case "terrainfeatures\\tree2_winter": // 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_fall": // from Tree
case "terrainfeatures\\tree3_spring": // from Tree case "terrainfeatures\\tree3_spring": // from Tree
case "terrainfeatures\\tree3_winter": // 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 assets
****/ ****/
// dynamic textures if (!ignoreWorld)
if (this.KeyStartsWith(key, "animals\\cat")) {
return this.ReloadPetOrHorseSprites<Cat>(content, key); // dynamic textures
if (this.KeyStartsWith(key, "animals\\dog")) if (this.KeyStartsWith(key, "animals\\cat"))
return this.ReloadPetOrHorseSprites<Dog>(content, key); return this.ReloadPetOrHorseSprites<Cat>(content, key);
if (this.IsInFolder(key, "Animals")) if (this.KeyStartsWith(key, "animals\\dog"))
return this.ReloadFarmAnimalSprites(content, key); return this.ReloadPetOrHorseSprites<Dog>(content, key);
if (this.IsInFolder(key, "Animals"))
return this.ReloadFarmAnimalSprites(content, key);
if (this.IsInFolder(key, "Buildings")) if (this.IsInFolder(key, "Buildings"))
return this.ReloadBuildings(content, key); return this.ReloadBuildings(content, key);
if (this.KeyStartsWith(key, "LooseSprites\\Fence")) if (this.KeyStartsWith(key, "LooseSprites\\Fence"))
return this.ReloadFenceTextures(key); return this.ReloadFenceTextures(key);
// dynamic data // dynamic data
if (this.IsInFolder(key, "Characters\\Dialogue")) if (this.IsInFolder(key, "Characters\\Dialogue"))
return this.ReloadNpcDialogue(key); return this.ReloadNpcDialogue(key);
if (this.IsInFolder(key, "Characters\\schedules")) if (this.IsInFolder(key, "Characters\\schedules"))
return this.ReloadNpcSchedules(key); return this.ReloadNpcSchedules(key);
}
return false; return false;
} }
@ -695,19 +708,23 @@ namespace StardewModdingAPI.Metadata
/// <summary>Reload map seat textures.</summary> /// <summary>Reload map seat textures.</summary>
/// <param name="content">The content manager through which to reload the asset.</param> /// <param name="content">The content manager through which to reload the asset.</param>
/// <param name="key">The asset key to reload.</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> /// <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); 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)) if (curKey == null || key.Equals(curKey, StringComparison.OrdinalIgnoreCase))
seat.overlayTexture = MapSeat.mapChairTexture; seat.overlayTexture = MapSeat.mapChairTexture;
}
} }
} }