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.cs573
1 files changed, 387 insertions, 186 deletions
diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs
index a64dc89b..1c0a04f0 100644
--- a/src/SMAPI/Metadata/CoreAssetPropagator.cs
+++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs
@@ -2,13 +2,13 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
-using System.Reflection;
using Microsoft.Xna.Framework.Graphics;
using StardewModdingAPI.Framework.Reflection;
using StardewValley;
using StardewValley.BellsAndWhistles;
using StardewValley.Buildings;
using StardewValley.Characters;
+using StardewValley.GameData.Movies;
using StardewValley.Locations;
using StardewValley.Menus;
using StardewValley.Objects;
@@ -25,8 +25,8 @@ namespace StardewModdingAPI.Metadata
/*********
** Fields
*********/
- /// <summary>Normalises an asset key to match the cache key.</summary>
- private readonly Func<string, string> GetNormalisedPath;
+ /// <summary>Normalizes an asset key to match the cache key and assert that it's valid.</summary>
+ private readonly Func<string, string> AssertAndNormalizeAssetName;
/// <summary>Simplifies access to private game code.</summary>
private readonly Reflector Reflection;
@@ -34,32 +34,72 @@ namespace StardewModdingAPI.Metadata
/// <summary>Encapsulates monitoring and logging.</summary>
private readonly IMonitor Monitor;
+ /// <summary>Optimized 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
*********/
- /// <summary>Initialise the core asset data.</summary>
- /// <param name="getNormalisedPath">Normalises an asset key to match the cache key.</param>
+ /// <summary>Initialize the core asset data.</summary>
+ /// <param name="assertAndNormalizeAssetName">Normalizes an asset key to match the cache key and assert that it's valid.</param>
/// <param name="reflection">Simplifies access to private code.</param>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
- public CoreAssetPropagator(Func<string, string> getNormalisedPath, Reflector reflection, IMonitor monitor)
+ public CoreAssetPropagator(Func<string, string> assertAndNormalizeAssetName, Reflector reflection, IMonitor monitor)
{
- this.GetNormalisedPath = getNormalisedPath;
+ this.AssertAndNormalizeAssetName = assertAndNormalizeAssetName;
this.Reflection = reflection;
this.Monitor = monitor;
}
/// <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 optimized 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,9 +111,9 @@ 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);
+ key = this.AssertAndNormalizeAssetName(key);
/****
** Special case: current map tilesheet
@@ -84,7 +124,7 @@ namespace StardewModdingAPI.Metadata
{
foreach (TileSheet tilesheet in Game1.currentLocation.map.TileSheets)
{
- if (this.GetNormalisedPath(tilesheet.ImageSource) == key)
+ if (this.NormalizeAssetNameIgnoringEmpty(tilesheet.ImageSource) == key)
Game1.mapDisplayDevice.LoadTileSheet(tilesheet);
}
}
@@ -97,22 +137,21 @@ namespace StardewModdingAPI.Metadata
bool anyChanged = false;
foreach (GameLocation location in this.GetLocations())
{
- if (!string.IsNullOrWhiteSpace(location.mapPath.Value) && this.GetNormalisedPath(location.mapPath.Value) == key)
+ if (!string.IsNullOrWhiteSpace(location.mapPath.Value) && this.NormalizeAssetNameIgnoringEmpty(location.mapPath.Value) == key)
{
- // reload map data
- this.Reflection.GetMethod(location, "reloadMap").Invoke();
- this.Reflection.GetMethod(location, "updateWarps").Invoke();
+ // general updates
+ location.reloadMap();
+ location.updateSeasonalTileSheets();
+ location.updateWarps();
- // reload doors
- {
- Type interiorDoorDictType = Type.GetType($"StardewValley.InteriorDoorDictionary, {Constants.GameAssemblyName}", throwOnError: true);
- ConstructorInfo constructor = interiorDoorDictType.GetConstructor(new[] { typeof(GameLocation) });
- if (constructor == null)
- throw new InvalidOperationException("Can't reset location doors: constructor not found for InteriorDoorDictionary type.");
- object instance = constructor.Invoke(new object[] { location });
+ // update interior doors
+ location.interiorDoors.Clear();
+ foreach (var entry in new InteriorDoorDictionary(location))
+ location.interiorDoors.Add(entry);
- this.Reflection.GetField<object>(location, "interiorDoors").SetValue(instance);
- }
+ // update doors
+ location.doors.Clear();
+ location.updateDoors();
anyChanged = true;
}
@@ -124,15 +163,11 @@ namespace StardewModdingAPI.Metadata
** Propagate by key
****/
Reflector reflection = this.Reflection;
- switch (key.ToLower().Replace("/", "\\")) // normalised key so we can compare statically
+ switch (key.ToLower().Replace("/", "\\")) // normalized key so we can compare statically
{
/****
** Animals
****/
- case "animals\\cat":
- return this.ReloadPetOrHorseSprites<Cat>(content, key);
- case "animals\\dog":
- return this.ReloadPetOrHorseSprites<Dog>(content, key);
case "animals\\horse":
return this.ReloadPetOrHorseSprites<Horse>(content, key);
@@ -146,204 +181,314 @@ namespace StardewModdingAPI.Metadata
/****
** Content\Characters\Farmer
****/
- case "characters\\farmer\\accessories": // Game1.loadContent
- return FarmerRenderer.accessoriesTexture = content.Load<Texture2D>(key);
+ case "characters\\farmer\\accessories": // Game1.LoadContent
+ FarmerRenderer.accessoriesTexture = content.Load<Texture2D>(key);
+ return true;
case "characters\\farmer\\farmer_base": // Farmer
+ case "characters\\farmer\\farmer_base_bald":
if (Game1.player == null || !Game1.player.IsMale)
return false;
- return Game1.player.FarmerRenderer = new FarmerRenderer(key);
+ Game1.player.FarmerRenderer = new FarmerRenderer(key, Game1.player);
+ return true;
case "characters\\farmer\\farmer_girl_base": // Farmer
+ case "characters\\farmer\\farmer_girl_bald":
if (Game1.player == null || Game1.player.IsMale)
return false;
- return Game1.player.FarmerRenderer = new FarmerRenderer(key);
+ Game1.player.FarmerRenderer = new FarmerRenderer(key, Game1.player);
+ return true;
- case "characters\\farmer\\hairstyles": // Game1.loadContent
- return FarmerRenderer.hairStylesTexture = content.Load<Texture2D>(key);
+ case "characters\\farmer\\hairstyles": // Game1.LoadContent
+ FarmerRenderer.hairStylesTexture = content.Load<Texture2D>(key);
+ return true;
- case "characters\\farmer\\hats": // Game1.loadContent
- return FarmerRenderer.hatsTexture = content.Load<Texture2D>(key);
+ case "characters\\farmer\\hats": // Game1.LoadContent
+ FarmerRenderer.hatsTexture = content.Load<Texture2D>(key);
+ return true;
- case "characters\\farmer\\shirts": // Game1.loadContent
- return FarmerRenderer.shirtsTexture = content.Load<Texture2D>(key);
+ case "characters\\farmer\\pants": // Game1.LoadContent
+ FarmerRenderer.pantsTexture = content.Load<Texture2D>(key);
+ return true;
+
+ case "characters\\farmer\\shirts": // Game1.LoadContent
+ FarmerRenderer.shirtsTexture = content.Load<Texture2D>(key);
+ return true;
/****
** Content\Data
****/
- case "data\\achievements": // Game1.loadContent
- return Game1.achievements = content.Load<Dictionary<int, string>>(key);
+ case "data\\achievements": // Game1.LoadContent
+ Game1.achievements = content.Load<Dictionary<int, string>>(key);
+ return true;
- case "data\\bigcraftablesinformation": // Game1.loadContent
- return Game1.bigCraftablesInformation = content.Load<Dictionary<int, string>>(key);
+ case "data\\bigcraftablesinformation": // Game1.LoadContent
+ Game1.bigCraftablesInformation = content.Load<Dictionary<int, string>>(key);
+ return true;
+
+ case "data\\clothinginformation": // Game1.LoadContent
+ Game1.clothingInformation = content.Load<Dictionary<int, string>>(key);
+ return true;
+
+ case "data\\concessiontastes": // MovieTheater.GetConcessionTasteForCharacter
+ this.Reflection
+ .GetField<List<ConcessionTaste>>(typeof(MovieTheater), "_concessionTastes")
+ .SetValue(content.Load<List<ConcessionTaste>>(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\\farmanimals": // FarmAnimal constructor
+ return this.ReloadFarmAnimalData();
+
+ case "data\\moviereactions": // MovieTheater.GetMovieReactions
+ this.Reflection
+ .GetField<List<MovieCharacterReaction>>(typeof(MovieTheater), "_genericReactions")
+ .SetValue(content.Load<List<MovieCharacterReaction>>(key));
+ return true;
+
+ case "data\\movies": // MovieTheater.GetMovieData
+ this.Reflection
+ .GetField<Dictionary<string, MovieData>>(typeof(MovieTheater), "_movieData")
+ .SetValue(content.Load<Dictionary<string, MovieData>>(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);
+ case "data\\npcgifttastes": // Game1.LoadContent
+ Game1.NPCGiftTastes = content.Load<Dictionary<string, string>>(key);
+ return true;
- case "data\\objectinformation": // Game1.loadContent
- return Game1.objectInformation = content.Load<Dictionary<int, string>>(key);
+ case "data\\objectcontexttags": // Game1.LoadContent
+ Game1.objectContextTags = content.Load<Dictionary<string, string>>(key);
+ return true;
+
+ case "data\\objectinformation": // Game1.LoadContent
+ Game1.objectInformation = content.Load<Dictionary<int, string>>(key);
+ return true;
/****
** Content\Fonts
****/
- case "fonts\\spritefont1": // Game1.loadContent
- return Game1.dialogueFont = content.Load<SpriteFont>(key);
+ case "fonts\\spritefont1": // Game1.LoadContent
+ Game1.dialogueFont = content.Load<SpriteFont>(key);
+ return true;
- case "fonts\\smallfont": // Game1.loadContent
- return Game1.smallFont = content.Load<SpriteFont>(key);
+ case "fonts\\smallfont": // Game1.LoadContent
+ Game1.smallFont = content.Load<SpriteFont>(key);
+ return true;
- case "fonts\\tinyfont": // Game1.loadContent
- return Game1.tinyFont = content.Load<SpriteFont>(key);
+ case "fonts\\tinyfont": // Game1.LoadContent
+ Game1.tinyFont = content.Load<SpriteFont>(key);
+ return true;
- case "fonts\\tinyfontborder": // Game1.loadContent
- return Game1.tinyFontBorder = content.Load<SpriteFont>(key);
+ case "fonts\\tinyfontborder": // Game1.LoadContent
+ Game1.tinyFontBorder = content.Load<SpriteFont>(key);
+ return true;
/****
- ** Content\Lighting
+ ** Content\LooseSprites\Lighting
****/
- case "loosesprites\\lighting\\greenlight": // Game1.loadContent
- return Game1.cauldronLight = content.Load<Texture2D>(key);
+ case "loosesprites\\lighting\\greenlight": // Game1.LoadContent
+ Game1.cauldronLight = content.Load<Texture2D>(key);
+ return true;
- case "loosesprites\\lighting\\indoorwindowlight": // Game1.loadContent
- return Game1.indoorWindowLight = content.Load<Texture2D>(key);
+ case "loosesprites\\lighting\\indoorwindowlight": // Game1.LoadContent
+ Game1.indoorWindowLight = content.Load<Texture2D>(key);
+ return true;
- case "loosesprites\\lighting\\lantern": // Game1.loadContent
- return Game1.lantern = content.Load<Texture2D>(key);
+ case "loosesprites\\lighting\\lantern": // Game1.LoadContent
+ Game1.lantern = content.Load<Texture2D>(key);
+ return true;
- case "loosesprites\\lighting\\sconcelight": // Game1.loadContent
- return Game1.sconceLight = content.Load<Texture2D>(key);
+ case "loosesprites\\lighting\\sconcelight": // Game1.LoadContent
+ Game1.sconceLight = content.Load<Texture2D>(key);
+ return true;
- case "loosesprites\\lighting\\windowlight": // Game1.loadContent
- return Game1.windowLight = content.Load<Texture2D>(key);
+ case "loosesprites\\lighting\\windowlight": // Game1.LoadContent
+ Game1.windowLight = content.Load<Texture2D>(key);
+ return true;
/****
** Content\LooseSprites
****/
- case "loosesprites\\controllermaps": // Game1.loadContent
- return Game1.controllerMaps = content.Load<Texture2D>(key);
+ case "loosesprites\\birds": // Game1.LoadContent
+ Game1.birdsSpriteSheet = content.Load<Texture2D>(key);
+ return true;
- case "loosesprites\\cursors": // Game1.loadContent
- return Game1.mouseCursors = content.Load<Texture2D>(key);
+ case "loosesprites\\concessions": // Game1.LoadContent
+ Game1.concessionsSpriteSheet = content.Load<Texture2D>(key);
+ return true;
- case "loosesprites\\daybg": // Game1.loadContent
- return Game1.daybg = content.Load<Texture2D>(key);
+ case "loosesprites\\controllermaps": // Game1.LoadContent
+ Game1.controllerMaps = content.Load<Texture2D>(key);
+ return true;
- case "loosesprites\\font_bold": // Game1.loadContent
- return SpriteText.spriteTexture = content.Load<Texture2D>(key);
+ case "loosesprites\\cursors": // Game1.LoadContent
+ Game1.mouseCursors = content.Load<Texture2D>(key);
+ foreach (DayTimeMoneyBox menu in Game1.onScreenMenus.OfType<DayTimeMoneyBox>())
+ {
+ foreach (ClickableTextureComponent button in new[] { menu.questButton, menu.zoomInButton, menu.zoomOutButton })
+ button.texture = Game1.mouseCursors;
+ }
+ return true;
- case "loosesprites\\font_colored": // Game1.loadContent
- return SpriteText.coloredTexture = content.Load<Texture2D>(key);
+ case "loosesprites\\cursors2": // Game1.LoadContent
+ Game1.mouseCursors2 = content.Load<Texture2D>(key);
+ return true;
- case "loosesprites\\nightbg": // Game1.loadContent
- return Game1.nightbg = content.Load<Texture2D>(key);
+ case "loosesprites\\daybg": // Game1.LoadContent
+ Game1.daybg = content.Load<Texture2D>(key);
+ return true;
+
+ case "loosesprites\\font_bold": // Game1.LoadContent
+ SpriteText.spriteTexture = content.Load<Texture2D>(key);
+ return true;
- case "loosesprites\\shadow": // Game1.loadContent
- return Game1.shadowTexture = content.Load<Texture2D>(key);
+ case "loosesprites\\font_colored": // Game1.LoadContent
+ SpriteText.coloredTexture = content.Load<Texture2D>(key);
+ return true;
+
+ case "loosesprites\\nightbg": // Game1.LoadContent
+ Game1.nightbg = content.Load<Texture2D>(key);
+ return true;
+
+ case "loosesprites\\shadow": // Game1.LoadContent
+ Game1.shadowTexture = content.Load<Texture2D>(key);
+ return true;
/****
- ** Content\Critters
+ ** Content\TileSheets
****/
- case "tilesheets\\crops": // Game1.loadContent
- return Game1.cropSpriteSheet = content.Load<Texture2D>(key);
+ case "tilesheets\\critters": // Critter constructor
+ this.ReloadCritterTextures(content, key);
+ return true;
- case "tilesheets\\debris": // Game1.loadContent
- return Game1.debrisSpriteSheet = content.Load<Texture2D>(key);
+ case "tilesheets\\crops": // Game1.LoadContent
+ Game1.cropSpriteSheet = content.Load<Texture2D>(key);
+ return true;
- case "tilesheets\\emotes": // Game1.loadContent
- return Game1.emoteSpriteSheet = content.Load<Texture2D>(key);
+ case "tilesheets\\debris": // Game1.LoadContent
+ Game1.debrisSpriteSheet = content.Load<Texture2D>(key);
+ return true;
- case "tilesheets\\furniture": // Game1.loadContent
- return Furniture.furnitureTexture = content.Load<Texture2D>(key);
+ case "tilesheets\\emotes": // Game1.LoadContent
+ Game1.emoteSpriteSheet = content.Load<Texture2D>(key);
+ return true;
- case "tilesheets\\projectiles": // Game1.loadContent
- return Projectile.projectileSheet = content.Load<Texture2D>(key);
+ case "tilesheets\\furniture": // Game1.LoadContent
+ Furniture.furnitureTexture = content.Load<Texture2D>(key);
+ return true;
- case "tilesheets\\rain": // Game1.loadContent
- return Game1.rainTexture = content.Load<Texture2D>(key);
+ case "tilesheets\\projectiles": // Game1.LoadContent
+ Projectile.projectileSheet = content.Load<Texture2D>(key);
+ return true;
+
+ case "tilesheets\\rain": // Game1.LoadContent
+ 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);
+ case "tilesheets\\weapons": // Game1.LoadContent
+ Tool.weaponsTexture = content.Load<Texture2D>(key);
+ return true;
/****
** Content\Maps
****/
- case "maps\\menutiles": // Game1.loadContent
- return Game1.menuTexture = content.Load<Texture2D>(key);
+ case "maps\\menutiles": // Game1.LoadContent
+ Game1.menuTexture = content.Load<Texture2D>(key);
+ return true;
+
+ case "maps\\menutilesuncolored": // Game1.LoadContent
+ Game1.uncoloredMenuTexture = content.Load<Texture2D>(key);
+ return true;
- case "maps\\springobjects": // Game1.loadContent
- return Game1.objectSpriteSheet = content.Load<Texture2D>(key);
+ case "maps\\springobjects": // Game1.LoadContent
+ 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
****/
case "minigames\\clouds": // TitleMenu
- if (Game1.activeClickableMenu is TitleMenu)
{
- reflection.GetField<Texture2D>(Game1.activeClickableMenu, "cloudsTexture").SetValue(content.Load<Texture2D>(key));
- return true;
+ if (Game1.activeClickableMenu is TitleMenu titleMenu)
+ {
+ titleMenu.cloudsTexture = content.Load<Texture2D>(key);
+ return true;
+ }
}
return false;
case "minigames\\titlebuttons": // TitleMenu
- if (Game1.activeClickableMenu is TitleMenu titleMenu)
{
- Texture2D texture = content.Load<Texture2D>(key);
- reflection.GetField<Texture2D>(titleMenu, "titleButtonsTexture").SetValue(texture);
- foreach (TemporaryAnimatedSprite bird in reflection.GetField<List<TemporaryAnimatedSprite>>(titleMenu, "birds").GetValue())
- bird.texture = texture;
- return true;
+ if (Game1.activeClickableMenu is TitleMenu titleMenu)
+ {
+ Texture2D texture = content.Load<Texture2D>(key);
+ titleMenu.titleButtonsTexture = texture;
+ foreach (TemporaryAnimatedSprite bird in titleMenu.birds)
+ bird.texture = texture;
+ return true;
+ }
}
return false;
/****
** Content\TileSheets
****/
- case "tilesheets\\animations": // Game1.loadContent
- return Game1.animations = content.Load<Texture2D>(key);
+ case "tilesheets\\animations": // Game1.LoadContent
+ Game1.animations = content.Load<Texture2D>(key);
+ return true;
- case "tilesheets\\buffsicons": // Game1.loadContent
- return Game1.buffsIcons = content.Load<Texture2D>(key);
+ case "tilesheets\\buffsicons": // Game1.LoadContent
+ 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)));
+ Bush.texture = new Lazy<Texture2D>(() => content.Load<Texture2D>(key));
return true;
- case "tilesheets\\craftables": // Game1.loadContent
- return Game1.bigCraftableSpriteSheet = content.Load<Texture2D>(key);
+ case "tilesheets\\craftables": // Game1.LoadContent
+ 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);
@@ -370,21 +515,19 @@ namespace StardewModdingAPI.Metadata
}
// 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.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);
@@ -411,7 +554,10 @@ namespace StardewModdingAPI.Metadata
where TAnimal : NPC
{
// find matches
- TAnimal[] animals = this.GetCharacters().OfType<TAnimal>().ToArray();
+ TAnimal[] animals = this.GetCharacters()
+ .OfType<TAnimal>()
+ .Where(p => key == this.NormalizeAssetNameIgnoringEmpty(p.Sprite?.Texture?.Name))
+ .ToArray();
if (!animals.Any())
return false;
@@ -478,6 +624,49 @@ namespace StardewModdingAPI.Metadata
return false;
}
+ /// <summary>Reload critter textures.</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 the number of reloaded assets.</returns>
+ private int ReloadCritterTextures(LocalizedContentManager content, string key)
+ {
+ // get critters
+ Critter[] critters =
+ (
+ from location in this.GetLocations()
+ let locCritters = this.Reflection.GetField<List<Critter>>(location, "critters").GetValue()
+ where locCritters != null
+ from Critter critter in locCritters
+ where this.NormalizeAssetNameIgnoringEmpty(critter.sprite?.Texture?.Name) == key
+ select critter
+ )
+ .ToArray();
+ if (!critters.Any())
+ return 0;
+
+ // update sprites
+ Texture2D texture = content.Load<Texture2D>(key);
+ foreach (var entry in critters)
+ this.SetSpriteTexture(entry.sprite, texture);
+
+ return critters.Length;
+ }
+
+ /// <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>
+ private bool ReloadFarmAnimalData()
+ {
+ bool changed = false;
+ foreach (FarmAnimal animal in this.GetFarmAnimals())
+ {
+ animal.reloadData();
+ changed = true;
+ }
+
+ return changed;
+ }
+
/// <summary>Reload the sprites for a fence type.</summary>
/// <param name="key">The asset key to reload.</param>
/// <returns>Returns whether any textures were reloaded.</returns>
@@ -501,7 +690,7 @@ namespace StardewModdingAPI.Metadata
// update fence textures
foreach (Fence fence in fences)
- this.Reflection.GetField<Lazy<Texture2D>>(fence, "fenceTexture").SetValue(new Lazy<Texture2D>(fence.loadFenceTexture));
+ fence.fenceTexture = new Lazy<Texture2D>(fence.loadFenceTexture);
return true;
}
@@ -511,71 +700,68 @@ namespace StardewModdingAPI.Metadata
/// <returns>Returns whether any NPCs were affected.</returns>
private bool ReloadNpcDispositions(LocalizedContentManager content, string key)
{
- IDictionary<string, string> dispositions = content.Load<Dictionary<string, string>>(key);
- foreach (NPC character in this.GetCharacters())
+ IDictionary<string, string> data = content.Load<Dictionary<string, string>>(key);
+ bool changed = false;
+ foreach (NPC npc in this.GetCharacters())
{
- if (!character.isVillager() || !dispositions.ContainsKey(character.Name))
- continue;
-
- NPC clone = new NPC(null, character.Position, character.DefaultMap, character.FacingDirection, character.Name, null, character.Portrait, eventActor: false);
- character.Age = clone.Age;
- character.Manners = clone.Manners;
- character.SocialAnxiety = clone.SocialAnxiety;
- character.Optimism = clone.Optimism;
- character.Gender = clone.Gender;
- character.datable.Value = clone.datable.Value;
- character.homeRegion = clone.homeRegion;
- character.Birthday_Season = clone.Birthday_Season;
- character.Birthday_Day = clone.Birthday_Day;
- character.id = clone.id;
- character.displayName = clone.displayName;
+ if (npc.isVillager() && data.ContainsKey(npc.Name))
+ {
+ npc.reloadData();
+ changed = true;
+ }
}
- return true;
+ return changed;
}
/// <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.NormalizeAssetNameIgnoringEmpty(npc.Sprite?.Texture?.Name)))
.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 = this
+ .GetCharacters()
+ .Where(npc => npc.isVillager() && lookup.Contains(this.NormalizeAssetNameIgnoringEmpty(npc.Portrait?.Name)))
.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 (NPC npc in villagers)
{
- villager.resetPortrait();
- villager.Portrait = texture;
+ npc.Portrait = content.Load<Texture2D>(npc.Portrait.Name);
+ reloaded++;
}
-
- return true;
+ return reloaded;
}
/// <summary>Reload tree textures.</summary>
@@ -594,7 +780,7 @@ namespace StardewModdingAPI.Metadata
{
Lazy<Texture2D> texture = new Lazy<Texture2D>(() => content.Load<Texture2D>(key));
foreach (Tree tree in trees)
- this.Reflection.GetField<Lazy<Texture2D>>(tree, "texture").SetValue(texture);
+ tree.texture = texture;
return true;
}
@@ -636,6 +822,8 @@ namespace StardewModdingAPI.Metadata
foreach (NPC villager in villagers)
{
// reload schedule
+ this.Reflection.GetField<bool>(villager, "_hasLoadedMasterScheduleData").SetValue(false);
+ this.Reflection.GetField<Dictionary<string, string>>(villager, "_masterScheduleData").SetValue(null);
villager.Schedule = villager.getSchedule(Game1.dayOfMonth);
if (villager.Schedule == null)
{
@@ -647,7 +835,7 @@ namespace StardewModdingAPI.Metadata
int lastScheduleTime = villager.Schedule.Keys.Where(p => p <= Game1.timeOfDay).OrderByDescending(p => p).FirstOrDefault();
if (lastScheduleTime != 0)
{
- this.Reflection.GetField<int>(villager, "scheduleTimeToTry").SetValue(this.Reflection.GetField<int>(typeof(NPC), "NO_TRY").GetValue()); // use time that's passed in to checkSchedule
+ villager.scheduleTimeToTry = NPC.NO_TRY; // use time that's passed in to checkSchedule
villager.checkSchedule(lastScheduleTime);
}
}
@@ -712,17 +900,30 @@ namespace StardewModdingAPI.Metadata
}
}
- /// <summary>Get whether a key starts with a substring after the substring is normalised.</summary>
+ /// <summary>Normalize an asset key to match the cache key and assert that it's valid, but don't raise an error for null or empty values.</summary>
+ /// <param name="path">The asset key to normalize.</param>
+ private string NormalizeAssetNameIgnoringEmpty(string path)
+ {
+ if (string.IsNullOrWhiteSpace(path))
+ return null;
+
+ return this.AssertAndNormalizeAssetName(path);
+ }
+
+ /// <summary>Get whether a key starts with a substring after the substring is normalized.</summary>
/// <param name="key">The key to check.</param>
- /// <param name="rawSubstring">The substring to normalise and find.</param>
+ /// <param name="rawSubstring">The substring to normalize and find.</param>
private bool KeyStartsWith(string key, string rawSubstring)
{
- return key.StartsWith(this.GetNormalisedPath(rawSubstring), StringComparison.InvariantCultureIgnoreCase);
+ if (string.IsNullOrWhiteSpace(key) || string.IsNullOrWhiteSpace(rawSubstring))
+ return false;
+
+ return key.StartsWith(this.NormalizeAssetNameIgnoringEmpty(rawSubstring), StringComparison.InvariantCultureIgnoreCase);
}
- /// <summary>Get whether a normalised asset key is in the given folder.</summary>
- /// <param name="key">The normalised asset key (like <c>Animals/cat</c>).</param>
- /// <param name="folder">The key folder (like <c>Animals</c>); doesn't need to be normalised.</param>
+ /// <summary>Get whether a normalized asset key is in the given folder.</summary>
+ /// <param name="key">The normalized asset key (like <c>Animals/cat</c>).</param>
+ /// <param name="folder">The key folder (like <c>Animals</c>); doesn't need to be normalized.</param>
/// <param name="allowSubfolders">Whether to return true if the key is inside a subfolder of the <paramref name="folder"/>.</param>
private bool IsInFolder(string key, string folder, bool allowSubfolders = false)
{