summaryrefslogtreecommitdiff
path: root/src/SMAPI
diff options
context:
space:
mode:
authorJesse Plamondon-Willard <github@jplamondonw.com>2018-03-26 09:35:34 -0400
committerJesse Plamondon-Willard <github@jplamondonw.com>2018-03-26 09:35:34 -0400
commit46141a7af21a921284bc82d49d888da864887d6e (patch)
treec71d17897377725f32653eacc65233f0b848f813 /src/SMAPI
parentafb3c49bbaab07f3148f70d54f5140cdd83f8c20 (diff)
parent4d68ef3514de7deb357a0042d1af7ccf241ab5ff (diff)
downloadSMAPI-46141a7af21a921284bc82d49d888da864887d6e.tar.gz
SMAPI-46141a7af21a921284bc82d49d888da864887d6e.tar.bz2
SMAPI-46141a7af21a921284bc82d49d888da864887d6e.zip
Merge branch 'develop' into stable
Diffstat (limited to 'src/SMAPI')
-rw-r--r--src/SMAPI/Constants.cs8
-rw-r--r--src/SMAPI/Framework/Content/AssetDataForImage.cs2
-rw-r--r--src/SMAPI/Framework/ContentCore.cs6
-rw-r--r--src/SMAPI/Metadata/CoreAssetPropagator.cs642
-rw-r--r--src/SMAPI/Metadata/CoreAssets.cs220
-rw-r--r--src/SMAPI/Program.cs31
-rw-r--r--src/SMAPI/StardewModdingAPI.csproj5
-rw-r--r--src/SMAPI/packages.config2
8 files changed, 681 insertions, 235 deletions
diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs
index d91fa5fb..6270186a 100644
--- a/src/SMAPI/Constants.cs
+++ b/src/SMAPI/Constants.cs
@@ -41,7 +41,7 @@ namespace StardewModdingAPI
#if STARDEW_VALLEY_1_3
new SemanticVersion($"2.6-alpha.{DateTime.UtcNow:yyyyMMddHHmm}");
#else
- new SemanticVersion($"2.5.3");
+ new SemanticVersion("2.5.4");
#endif
/// <summary>The minimum supported version of Stardew Valley.</summary>
@@ -49,7 +49,7 @@ namespace StardewModdingAPI
#if STARDEW_VALLEY_1_3
new GameVersion("1.3.0.4");
#else
- new SemanticVersion("1.2.33");
+ new SemanticVersion("1.2.30");
#endif
/// <summary>The maximum supported version of Stardew Valley.</summary>
@@ -81,8 +81,8 @@ namespace StardewModdingAPI
/****
** Internal
****/
- /// <summary>The GitHub repository to check for updates.</summary>
- internal const string GitHubRepository = "Pathoschild/SMAPI";
+ /// <summary>The URL of the SMAPI home page.</summary>
+ internal const string HomePageUrl = "https://smapi.io";
/// <summary>The file path for the SMAPI configuration file.</summary>
internal static string ApiConfigPath => Path.Combine(Constants.ExecutionPath, $"{typeof(Program).Assembly.GetName().Name}.config.json");
diff --git a/src/SMAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs
index c665484f..1eef2afb 100644
--- a/src/SMAPI/Framework/Content/AssetDataForImage.cs
+++ b/src/SMAPI/Framework/Content/AssetDataForImage.cs
@@ -58,7 +58,7 @@ namespace StardewModdingAPI.Framework.Content
for (int i = 0; i < sourceData.Length; i++)
{
Color pixel = sourceData[i];
- if (pixel.A != 0) // not transparent
+ if (pixel.A > 4) // not transparent (note: on Linux/Mac, fully transparent pixels may have an alpha up to 4 for some reason)
newData[i] = pixel;
}
sourceData = newData;
diff --git a/src/SMAPI/Framework/ContentCore.cs b/src/SMAPI/Framework/ContentCore.cs
index 85b8db8f..3c7e7b5a 100644
--- a/src/SMAPI/Framework/ContentCore.cs
+++ b/src/SMAPI/Framework/ContentCore.cs
@@ -51,7 +51,7 @@ namespace StardewModdingAPI.Framework
private readonly IDictionary<string, LocalizedContentManager.LanguageCode> LanguageCodes;
/// <summary>Provides metadata for core game assets.</summary>
- private readonly CoreAssets CoreAssets;
+ private readonly CoreAssetPropagator CoreAssets;
/// <summary>The assets currently being intercepted by <see cref="IAssetLoader"/> instances. This is used to prevent infinite loops when a loader loads a new asset.</summary>
private readonly ContextHash<string> AssetsBeingLoaded = new ContextHash<string>();
@@ -103,7 +103,7 @@ namespace StardewModdingAPI.Framework
this.ModContentPrefix = this.GetAssetNameFromFilePath(Constants.ModPath);
// get asset data
- this.CoreAssets = new CoreAssets(this.NormaliseAssetName, reflection);
+ this.CoreAssets = new CoreAssetPropagator(this.NormaliseAssetName, reflection);
this.Locales = this.GetKeyLocales(reflection);
this.LanguageCodes = this.Locales.ToDictionary(p => p.Value, p => p.Key, StringComparer.InvariantCultureIgnoreCase);
}
@@ -368,7 +368,7 @@ namespace StardewModdingAPI.Framework
int reloaded = 0;
foreach (string key in removeAssetNames)
{
- if (this.CoreAssets.ReloadForKey(Game1.content, key)) // use an intercepted content manager
+ if (this.CoreAssets.Propagate(Game1.content, key)) // use an intercepted content manager
reloaded++;
}
diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs
new file mode 100644
index 00000000..e54e0286
--- /dev/null
+++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs
@@ -0,0 +1,642 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Microsoft.Xna.Framework.Graphics;
+using StardewModdingAPI.Framework.Reflection;
+using StardewValley;
+using StardewValley.BellsAndWhistles;
+using StardewValley.Buildings;
+using StardewValley.Characters;
+using StardewValley.Locations;
+using StardewValley.Menus;
+using StardewValley.Objects;
+using StardewValley.Projectiles;
+using StardewValley.TerrainFeatures;
+
+namespace StardewModdingAPI.Metadata
+{
+ /// <summary>Propagates changes to core assets to the game state.</summary>
+ internal class CoreAssetPropagator
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>Normalises an asset key to match the cache key.</summary>
+ private readonly Func<string, string> GetNormalisedPath;
+
+ /// <summary>Simplifies access to private game code.</summary>
+ private readonly Reflector Reflection;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Initialise the core asset data.</summary>
+ /// <param name="getNormalisedPath">Normalises an asset key to match the cache key.</param>
+ /// <param name="reflection">Simplifies access to private code.</param>
+ public CoreAssetPropagator(Func<string, string> getNormalisedPath, Reflector reflection)
+ {
+ this.GetNormalisedPath = getNormalisedPath;
+ this.Reflection = reflection;
+ }
+
+ /// <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>
+ /// <returns>Returns whether an asset was reloaded.</returns>
+ public bool Propagate(LocalizedContentManager content, string key)
+ {
+ object result = this.PropagateImpl(content, key);
+ if (result is bool b)
+ return b;
+ return result != null;
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <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>
+ /// <returns>Returns any non-null value to indicate an asset was loaded.</returns>
+ private object PropagateImpl(LocalizedContentManager content, string key)
+ {
+ Reflector reflection = this.Reflection;
+ switch (key.ToLower().Replace("/", "\\")) // normalised 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);
+
+ /****
+ ** Buildings
+ ****/
+ case "buildings\\houses": // Farm
+#if STARDEW_VALLEY_1_3
+ reflection.GetField<Texture2D>(typeof(Farm), nameof(Farm.houseTextures)).SetValue(content.Load<Texture2D>(key));
+ return true;
+#else
+ {
+ Farm farm = Game1.getFarm();
+ if (farm == null)
+ return false;
+ return farm.houseTextures = content.Load<Texture2D>(key);
+ }
+#endif
+
+ /****
+ ** Content\Characters\Farmer
+ ****/
+ case "characters\\farmer\\accessories": // Game1.loadContent
+ return FarmerRenderer.accessoriesTexture = content.Load<Texture2D>(key);
+
+ case "characters\\farmer\\farmer_base": // Farmer
+ if (Game1.player == null || !Game1.player.isMale)
+ return false;
+#if STARDEW_VALLEY_1_3
+ return Game1.player.FarmerRenderer = new FarmerRenderer(key);
+#else
+ return Game1.player.FarmerRenderer = new FarmerRenderer(content.Load<Texture2D>(key));
+#endif
+
+ case "characters\\farmer\\farmer_girl_base": // Farmer
+ if (Game1.player == null || Game1.player.isMale)
+ return false;
+#if STARDEW_VALLEY_1_3
+ return Game1.player.FarmerRenderer = new FarmerRenderer(key);
+#else
+ return Game1.player.FarmerRenderer = new FarmerRenderer(content.Load<Texture2D>(key));
+#endif
+
+ case "characters\\farmer\\hairstyles": // Game1.loadContent
+ return FarmerRenderer.hairStylesTexture = content.Load<Texture2D>(key);
+
+ case "characters\\farmer\\hats": // Game1.loadContent
+ return FarmerRenderer.hatsTexture = content.Load<Texture2D>(key);
+
+ case "characters\\farmer\\shirts": // Game1.loadContent
+ return FarmerRenderer.shirtsTexture = content.Load<Texture2D>(key);
+
+ /****
+ ** Content\Data
+ ****/
+ case "data\\achievements": // Game1.loadContent
+ return Game1.achievements = content.Load<Dictionary<int, string>>(key);
+
+ case "data\\bigcraftablesinformation": // Game1.loadContent
+ return Game1.bigCraftablesInformation = content.Load<Dictionary<int, string>>(key);
+
+ case "data\\cookingrecipes": // CraftingRecipe.InitShared
+ return CraftingRecipe.cookingRecipes = content.Load<Dictionary<string, string>>(key);
+
+ case "data\\craftingrecipes": // CraftingRecipe.InitShared
+ return CraftingRecipe.craftingRecipes = content.Load<Dictionary<string, string>>(key);
+
+ case "data\\npcgifttastes": // Game1.loadContent
+ return Game1.NPCGiftTastes = content.Load<Dictionary<string, string>>(key);
+
+ case "data\\objectinformation": // Game1.loadContent
+ return Game1.objectInformation = content.Load<Dictionary<int, string>>(key);
+
+ /****
+ ** Content\Fonts
+ ****/
+ case "fonts\\spritefont1": // Game1.loadContent
+ return Game1.dialogueFont = content.Load<SpriteFont>(key);
+
+ case "fonts\\smallfont": // Game1.loadContent
+ return Game1.smallFont = content.Load<SpriteFont>(key);
+
+ case "fonts\\tinyfont": // Game1.loadContent
+ return Game1.tinyFont = content.Load<SpriteFont>(key);
+
+ case "fonts\\tinyfontborder": // Game1.loadContent
+ return Game1.tinyFontBorder = content.Load<SpriteFont>(key);
+
+ /****
+ ** Content\Lighting
+ ****/
+ case "loosesprites\\lighting\\greenlight": // Game1.loadContent
+ return Game1.cauldronLight = content.Load<Texture2D>(key);
+
+ case "loosesprites\\lighting\\indoorwindowlight": // Game1.loadContent
+ return Game1.indoorWindowLight = content.Load<Texture2D>(key);
+
+ case "loosesprites\\lighting\\lantern": // Game1.loadContent
+ return Game1.lantern = content.Load<Texture2D>(key);
+
+ case "loosesprites\\lighting\\sconcelight": // Game1.loadContent
+ return Game1.sconceLight = content.Load<Texture2D>(key);
+
+ case "loosesprites\\lighting\\windowlight": // Game1.loadContent
+ return Game1.windowLight = content.Load<Texture2D>(key);
+
+ /****
+ ** Content\LooseSprites
+ ****/
+ case "loosesprites\\controllermaps": // Game1.loadContent
+ return Game1.controllerMaps = content.Load<Texture2D>(key);
+
+ case "loosesprites\\cursors": // Game1.loadContent
+ return Game1.mouseCursors = content.Load<Texture2D>(key);
+
+ case "loosesprites\\daybg": // Game1.loadContent
+ return Game1.daybg = content.Load<Texture2D>(key);
+
+ case "loosesprites\\font_bold": // Game1.loadContent
+ return SpriteText.spriteTexture = content.Load<Texture2D>(key);
+
+ case "loosesprites\\font_colored": // Game1.loadContent
+ return SpriteText.coloredTexture = content.Load<Texture2D>(key);
+
+ case "loosesprites\\nightbg": // Game1.loadContent
+ return Game1.nightbg = content.Load<Texture2D>(key);
+
+ case "loosesprites\\shadow": // Game1.loadContent
+ return Game1.shadowTexture = content.Load<Texture2D>(key);
+
+ /****
+ ** Content\Critters
+ ****/
+#if !STARDEW_VALLEY_1_3
+ case "tilesheets\\critters": // Criter.InitShared
+ return Critter.critterTexture = content.Load<Texture2D>(key);
+#endif
+
+ case "tilesheets\\crops": // Game1.loadContent
+ return Game1.cropSpriteSheet = content.Load<Texture2D>(key);
+
+ case "tilesheets\\debris": // Game1.loadContent
+ return Game1.debrisSpriteSheet = content.Load<Texture2D>(key);
+
+ case "tilesheets\\emotes": // Game1.loadContent
+ return Game1.emoteSpriteSheet = content.Load<Texture2D>(key);
+
+ case "tilesheets\\furniture": // Game1.loadContent
+ return Furniture.furnitureTexture = content.Load<Texture2D>(key);
+
+ case "tilesheets\\projectiles": // Game1.loadContent
+ return Projectile.projectileSheet = content.Load<Texture2D>(key);
+
+ case "tilesheets\\rain": // Game1.loadContent
+ return Game1.rainTexture = content.Load<Texture2D>(key);
+
+ case "tilesheets\\tools": // Game1.ResetToolSpriteSheet
+ Game1.ResetToolSpriteSheet();
+ return true;
+
+ case "tilesheets\\weapons": // Game1.loadContent
+ return Tool.weaponsTexture = content.Load<Texture2D>(key);
+
+ /****
+ ** Content\Maps
+ ****/
+ case "maps\\menutiles": // Game1.loadContent
+ return Game1.menuTexture = content.Load<Texture2D>(key);
+
+ case "maps\\springobjects": // Game1.loadContent
+ return Game1.objectSpriteSheet = content.Load<Texture2D>(key);
+
+ case "maps\\walls_and_floors": // Wallpaper
+ return Wallpaper.wallpaperTexture = content.Load<Texture2D>(key);
+
+ /****
+ ** Content\Minigames
+ ****/
+ case "minigames\\clouds": // TitleMenu
+ if (Game1.activeClickableMenu is TitleMenu)
+ {
+ reflection.GetField<Texture2D>(Game1.activeClickableMenu, "cloudsTexture").SetValue(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())
+#if STARDEW_VALLEY_1_3
+ bird.texture = texture;
+#else
+ bird.Texture = texture;
+#endif
+ return true;
+ }
+ return false;
+
+ /****
+ ** Content\TileSheets
+ ****/
+ case "tilesheets\\animations": // Game1.loadContent
+ return Game1.animations = content.Load<Texture2D>(key);
+
+ case "tilesheets\\buffsicons": // Game1.loadContent
+ return Game1.buffsIcons = content.Load<Texture2D>(key);
+
+ case "tilesheets\\bushes": // new Bush()
+#if STARDEW_VALLEY_1_3
+ reflection.GetField<Lazy<Texture2D>>(typeof(Bush), "texture").SetValue(new Lazy<Texture2D>(() => content.Load<Texture2D>(key)));
+ return true;
+#else
+ return Bush.texture = content.Load<Texture2D>(key);
+#endif
+
+ case "tilesheets\\craftables": // Game1.loadContent
+ return Game1.bigCraftableSpriteSheet = content.Load<Texture2D>(key);
+
+ case "tilesheets\\fruittrees": // FruitTree
+ return FruitTree.texture = content.Load<Texture2D>(key);
+
+ /****
+ ** Content\TerrainFeatures
+ ****/
+ case "terrainfeatures\\flooring": // Flooring
+ return Flooring.floorsTexture = content.Load<Texture2D>(key);
+
+ case "terrainfeatures\\hoedirt": // from HoeDirt
+ return HoeDirt.lightTexture = content.Load<Texture2D>(key);
+
+ case "terrainfeatures\\hoedirtdark": // from HoeDirt
+ return HoeDirt.darkTexture = content.Load<Texture2D>(key);
+
+ case "terrainfeatures\\hoedirtsnow": // from HoeDirt
+ return HoeDirt.snowTexture = content.Load<Texture2D>(key);
+
+ case "terrainfeatures\\mushroom_tree": // from Tree
+ return this.ReloadTreeTextures(content, key, Tree.mushroomTree);
+
+ case "terrainfeatures\\tree_palm": // from Tree
+ return 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);
+
+ 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);
+
+ 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);
+ }
+
+ // dynamic textures
+ 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"))
+ return this.ReloadNpcSprites(content, key, monster: false);
+
+ if (this.IsInFolder(key, "Characters\\Monsters"))
+ return this.ReloadNpcSprites(content, key, monster: true);
+
+ if (key.StartsWith(this.GetNormalisedPath("LooseSprites\\Fence"), StringComparison.InvariantCultureIgnoreCase))
+ return this.ReloadFenceTextures(content, key);
+
+ if (this.IsInFolder(key, "Portraits"))
+ return this.ReloadNpcPortraits(content, key);
+
+ return false;
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /****
+ ** Reload methods
+ ****/
+ /// <summary>Reload the sprites for matching pets or horses.</summary>
+ /// <typeparam name="TAnimal">The animal type.</typeparam>
+ /// <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 ReloadPetOrHorseSprites<TAnimal>(LocalizedContentManager content, string key)
+ where TAnimal : NPC
+ {
+ // find matches
+ TAnimal[] animals = this.GetCharacters().OfType<TAnimal>().ToArray();
+ if (!animals.Any())
+ return false;
+
+ // update sprites
+ Texture2D texture = content.Load<Texture2D>(key);
+ foreach (TAnimal animal in animals)
+ this.SetSpriteTexture(animal.sprite, texture);
+ return true;
+ }
+
+ /// <summary>Reload the sprites for matching farm animals.</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>
+ /// <remarks>Derived from <see cref="FarmAnimal.reload"/>.</remarks>
+ private bool ReloadFarmAnimalSprites(LocalizedContentManager content, string key)
+ {
+ // find matches
+ FarmAnimal[] animals = this.GetFarmAnimals().ToArray();
+ if (!animals.Any())
+ return false;
+
+ // update sprites
+ Lazy<Texture2D> texture = new Lazy<Texture2D>(() => content.Load<Texture2D>(key));
+ foreach (FarmAnimal animal in animals)
+ {
+ // get expected key
+ string expectedKey = animal.age < animal.ageWhenMature
+ ? $"Baby{(animal.type == "Duck" ? "White Chicken" : animal.type)}"
+ : animal.type;
+ if (animal.showDifferentTextureWhenReadyForHarvest && animal.currentProduce <= 0)
+ expectedKey = $"Sheared{expectedKey}";
+ expectedKey = $"Animals\\{expectedKey}";
+
+ // reload asset
+ if (expectedKey == key)
+ this.SetSpriteTexture(animal.sprite, texture.Value);
+ }
+ return texture.IsValueCreated;
+ }
+
+ /// <summary>Reload building 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 whether any textures were reloaded.</returns>
+ private bool ReloadBuildings(LocalizedContentManager content, string key)
+ {
+ // get buildings
+ string type = Path.GetFileName(key);
+ Building[] buildings = Game1.locations
+ .OfType<BuildableGameLocation>()
+ .SelectMany(p => p.buildings)
+ .Where(p => p.buildingType == type)
+ .ToArray();
+
+ // reload buildings
+ if (buildings.Any())
+ {
+ Lazy<Texture2D> texture = new Lazy<Texture2D>(() => content.Load<Texture2D>(key));
+ foreach (Building building in buildings)
+#if STARDEW_VALLEY_1_3
+ building.texture = texture;
+#else
+ building.texture = texture.Value;
+#endif
+ return true;
+ }
+ return false;
+ }
+
+ /// <summary>Reload the sprites for a fence type.</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 ReloadFenceTextures(LocalizedContentManager content, string key)
+ {
+ // get fence type
+ if (!int.TryParse(this.GetSegments(key)[1].Substring("Fence".Length), out int fenceType))
+ return false;
+
+ // get fences
+ Fence[] fences =
+ (
+ from location in this.GetLocations()
+ from fence in location.Objects.Values.OfType<Fence>()
+ where fenceType == 1
+ ? fence.isGate
+ : fence.whichType == fenceType
+ select fence
+ )
+ .ToArray();
+
+ // update fence textures
+ foreach (Fence fence in fences)
+ fence.reloadSprite();
+ return true;
+ }
+
+ /// <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>
+ /// <param name="monster">Whether to match monsters (<c>true</c>) or non-monsters (<c>false</c>).</param>
+ /// <returns>Returns whether any textures were reloaded.</returns>
+ private bool ReloadNpcSprites(LocalizedContentManager content, string key, bool monster)
+ {
+ // get NPCs
+ string name = this.GetNpcNameFromFileName(Path.GetFileName(key));
+ NPC[] characters = this.GetCharacters().Where(npc => npc.name == name && npc.IsMonster == monster).ToArray();
+ if (!characters.Any())
+ return false;
+
+ // update portrait
+ Texture2D texture = content.Load<Texture2D>(key);
+ foreach (NPC character in characters)
+ this.SetSpriteTexture(character.Sprite, texture);
+ return true;
+ }
+
+ /// <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)
+ {
+ // get NPCs
+ string name = this.GetNpcNameFromFileName(Path.GetFileName(key));
+ NPC[] villagers = this.GetCharacters().Where(npc => npc.name == name && npc.isVillager()).ToArray();
+ if (!villagers.Any())
+ return false;
+
+ // update portrait
+ Texture2D texture = content.Load<Texture2D>(key);
+ foreach (NPC villager in villagers)
+ villager.Portrait = texture;
+ return true;
+ }
+
+ /// <summary>Reload tree 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="type">The type to reload.</param>
+ /// <returns>Returns whether any textures were reloaded.</returns>
+ private bool ReloadTreeTextures(LocalizedContentManager content, string key, int type)
+ {
+ Tree[] trees = Game1.locations
+ .SelectMany(p => p.terrainFeatures.Values.OfType<Tree>())
+ .Where(tree => tree.treeType == type)
+ .ToArray();
+
+ if (trees.Any())
+ {
+ Lazy<Texture2D> texture = new Lazy<Texture2D>(() => content.Load<Texture2D>(key));
+ foreach (Tree tree in trees)
+#if STARDEW_VALLEY_1_3
+ this.Reflection.GetField<Lazy<Texture2D>>(tree, "texture").SetValue(texture);
+#else
+ this.Reflection.GetField<Texture2D>(tree, "texture").SetValue(texture.Value);
+#endif
+ return true;
+ }
+
+ return false;
+ }
+
+ /****
+ ** Helpers
+ ****/
+ /// <summary>Reload the texture for an animated sprite.</summary>
+ /// <param name="sprite">The animated sprite to update.</param>
+ /// <param name="texture">The texture to set.</param>
+ private void SetSpriteTexture(AnimatedSprite sprite, Texture2D texture)
+ {
+#if STARDEW_VALLEY_1_3
+ this.Reflection.GetField<Texture2D>(sprite, "spriteTexture").SetValue(texture);
+#else
+ sprite.Texture = texture;
+#endif
+ }
+
+ /// <summary>Get an NPC name from the name of their file under <c>Content/Characters</c>.</summary>
+ /// <param name="name">The file name.</param>
+ /// <remarks>Derived from <see cref="NPC.reloadSprite"/>.</remarks>
+ private string GetNpcNameFromFileName(string name)
+ {
+ switch (name)
+ {
+ case "Mariner":
+ return "Old Mariner";
+ case "DwarfKing":
+ return "Dwarf King";
+ case "MrQi":
+ return "Mister Qi";
+ default:
+ return name;
+ }
+ }
+
+ /// <summary>Get all NPCs in the game (excluding farm animals).</summary>
+ private IEnumerable<NPC> GetCharacters()
+ {
+ return this.GetLocations().SelectMany(p => p.characters);
+ }
+
+ /// <summary>Get all farm animals in the game.</summary>
+ private IEnumerable<FarmAnimal> GetFarmAnimals()
+ {
+ foreach (GameLocation location in this.GetLocations())
+ {
+ if (location is Farm farm)
+ {
+ foreach (FarmAnimal animal in farm.animals.Values)
+ yield return animal;
+ }
+ else if (location is AnimalHouse animalHouse)
+ foreach (FarmAnimal animal in animalHouse.animals.Values)
+ yield return animal;
+ }
+ }
+
+ /// <summary>Get all locations in the game.</summary>
+ private IEnumerable<GameLocation> GetLocations()
+ {
+ foreach (GameLocation location in Game1.locations)
+ {
+ yield return location;
+
+ if (location is BuildableGameLocation buildableLocation)
+ {
+ foreach (Building building in buildableLocation.buildings)
+ {
+ if (building.indoors != null)
+ yield return building.indoors;
+ }
+ }
+ }
+ }
+
+ /// <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>
+ /// <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)
+ {
+ return
+ key.StartsWith(this.GetNormalisedPath($"{folder}\\"), StringComparison.InvariantCultureIgnoreCase)
+ && (allowSubfolders || this.CountSegments(key) == this.CountSegments(folder) + 1);
+ }
+
+ /// <summary>Get the segments in a path (e.g. 'a/b' is 'a' and 'b').</summary>
+ /// <param name="path">The path to check.</param>
+ private string[] GetSegments(string path)
+ {
+ if (path == null)
+ return new string[0];
+ return path.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
+ }
+
+ /// <summary>Count the number of segments in a path (e.g. 'a/b' is 2).</summary>
+ /// <param name="path">The path to check.</param>
+ private int CountSegments(string path)
+ {
+ return this.GetSegments(path).Length;
+ }
+ }
+}
diff --git a/src/SMAPI/Metadata/CoreAssets.cs b/src/SMAPI/Metadata/CoreAssets.cs
deleted file mode 100644
index 87629682..00000000
--- a/src/SMAPI/Metadata/CoreAssets.cs
+++ /dev/null
@@ -1,220 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using Microsoft.Xna.Framework.Graphics;
-using StardewModdingAPI.Framework.Reflection;
-using StardewValley;
-using StardewValley.BellsAndWhistles;
-using StardewValley.Buildings;
-using StardewValley.Locations;
-using StardewValley.Menus;
-using StardewValley.Objects;
-using StardewValley.Projectiles;
-using StardewValley.TerrainFeatures;
-
-namespace StardewModdingAPI.Metadata
-{
- /// <summary>Provides metadata about core assets in the game.</summary>
- internal class CoreAssets
- {
- /*********
- ** Properties
- *********/
- /// <summary>Normalises an asset key to match the cache key.</summary>
- protected readonly Func<string, string> GetNormalisedPath;
-
- /// <summary>Setters which update static or singleton texture fields indexed by normalised asset key.</summary>
- private readonly IDictionary<string, Action<LocalizedContentManager, string>> SingletonSetters;
-
-
- /*********
- ** Public methods
- *********/
- /// <summary>Initialise the core asset data.</summary>
- /// <param name="getNormalisedPath">Normalises an asset key to match the cache key.</param>
- /// <param name="reflection">Simplifies access to private code.</param>
- public CoreAssets(Func<string, string> getNormalisedPath, Reflector reflection)
- {
- this.GetNormalisedPath = getNormalisedPath;
- this.SingletonSetters =
- new Dictionary<string, Action<LocalizedContentManager, string>>
- {
- // from CraftingRecipe.InitShared
- ["Data\\CraftingRecipes"] = (content, key) => CraftingRecipe.craftingRecipes = content.Load<Dictionary<string, string>>(key),
- ["Data\\CookingRecipes"] = (content, key) => CraftingRecipe.cookingRecipes = content.Load<Dictionary<string, string>>(key),
-
- // from Game1.loadContent
- ["LooseSprites\\daybg"] = (content, key) => Game1.daybg = content.Load<Texture2D>(key),
- ["LooseSprites\\nightbg"] = (content, key) => Game1.nightbg = content.Load<Texture2D>(key),
- ["Maps\\MenuTiles"] = (content, key) => Game1.menuTexture = content.Load<Texture2D>(key),
- ["LooseSprites\\Lighting\\lantern"] = (content, key) => Game1.lantern = content.Load<Texture2D>(key),
- ["LooseSprites\\Lighting\\windowLight"] = (content, key) => Game1.windowLight = content.Load<Texture2D>(key),
- ["LooseSprites\\Lighting\\sconceLight"] = (content, key) => Game1.sconceLight = content.Load<Texture2D>(key),
- ["LooseSprites\\Lighting\\greenLight"] = (content, key) => Game1.cauldronLight = content.Load<Texture2D>(key),
- ["LooseSprites\\Lighting\\indoorWindowLight"] = (content, key) => Game1.indoorWindowLight = content.Load<Texture2D>(key),
- ["LooseSprites\\shadow"] = (content, key) => Game1.shadowTexture = content.Load<Texture2D>(key),
- ["LooseSprites\\Cursors"] = (content, key) => Game1.mouseCursors = content.Load<Texture2D>(key),
- ["LooseSprites\\ControllerMaps"] = (content, key) => Game1.controllerMaps = content.Load<Texture2D>(key),
- ["TileSheets\\animations"] = (content, key) => Game1.animations = content.Load<Texture2D>(key),
- ["Data\\Achievements"] = (content, key) => Game1.achievements = content.Load<Dictionary<int, string>>(key),
- ["Data\\NPCGiftTastes"] = (content, key) => Game1.NPCGiftTastes = content.Load<Dictionary<string, string>>(key),
- ["Fonts\\SpriteFont1"] = (content, key) => Game1.dialogueFont = content.Load<SpriteFont>(key),
- ["Fonts\\SmallFont"] = (content, key) => Game1.smallFont = content.Load<SpriteFont>(key),
- ["Fonts\\tinyFont"] = (content, key) => Game1.tinyFont = content.Load<SpriteFont>(key),
- ["Fonts\\tinyFontBorder"] = (content, key) => Game1.tinyFontBorder = content.Load<SpriteFont>(key),
- ["Maps\\springobjects"] = (content, key) => Game1.objectSpriteSheet = content.Load<Texture2D>(key),
- ["TileSheets\\crops"] = (content, key) => Game1.cropSpriteSheet = content.Load<Texture2D>(key),
- ["TileSheets\\emotes"] = (content, key) => Game1.emoteSpriteSheet = content.Load<Texture2D>(key),
- ["TileSheets\\debris"] = (content, key) => Game1.debrisSpriteSheet = content.Load<Texture2D>(key),
- ["TileSheets\\Craftables"] = (content, key) => Game1.bigCraftableSpriteSheet = content.Load<Texture2D>(key),
- ["TileSheets\\rain"] = (content, key) => Game1.rainTexture = content.Load<Texture2D>(key),
- ["TileSheets\\BuffsIcons"] = (content, key) => Game1.buffsIcons = content.Load<Texture2D>(key),
- ["Data\\ObjectInformation"] = (content, key) => Game1.objectInformation = content.Load<Dictionary<int, string>>(key),
- ["Data\\BigCraftablesInformation"] = (content, key) => Game1.bigCraftablesInformation = content.Load<Dictionary<int, string>>(key),
- ["Characters\\Farmer\\hairstyles"] = (content, key) => FarmerRenderer.hairStylesTexture = content.Load<Texture2D>(key),
- ["Characters\\Farmer\\shirts"] = (content, key) => FarmerRenderer.shirtsTexture = content.Load<Texture2D>(key),
- ["Characters\\Farmer\\hats"] = (content, key) => FarmerRenderer.hatsTexture = content.Load<Texture2D>(key),
- ["Characters\\Farmer\\accessories"] = (content, key) => FarmerRenderer.accessoriesTexture = content.Load<Texture2D>(key),
- ["TileSheets\\furniture"] = (content, key) => Furniture.furnitureTexture = content.Load<Texture2D>(key),
- ["LooseSprites\\font_bold"] = (content, key) => SpriteText.spriteTexture = content.Load<Texture2D>(key),
- ["LooseSprites\\font_colored"] = (content, key) => SpriteText.coloredTexture = content.Load<Texture2D>(key),
- ["TileSheets\\weapons"] = (content, key) => Tool.weaponsTexture = content.Load<Texture2D>(key),
- ["TileSheets\\Projectiles"] = (content, key) => Projectile.projectileSheet = content.Load<Texture2D>(key),
-
- // from Game1.ResetToolSpriteSheet
- ["TileSheets\\tools"] = (content, key) => Game1.ResetToolSpriteSheet(),
-
-#if STARDEW_VALLEY_1_3
- // from Bush
- ["TileSheets\\bushes"] = (content, key) => reflection.GetField<Lazy<Texture2D>>(typeof(Bush), "texture").SetValue(new Lazy<Texture2D>(() => content.Load<Texture2D>(key))),
-
- // from Farm
- ["Buildings\\houses"] = (content, key) => reflection.GetField<Texture2D>(typeof(Farm), nameof(Farm.houseTextures)).SetValue(content.Load<Texture2D>(key)),
-
- // from Farmer
- ["Characters\\Farmer\\farmer_base"] = (content, key) =>
- {
- if (Game1.player != null && Game1.player.isMale)
- Game1.player.FarmerRenderer = new FarmerRenderer(key);
- },
- ["Characters\\Farmer\\farmer_girl_base"] = (content, key) =>
- {
- if (Game1.player != null && !Game1.player.isMale)
- Game1.player.FarmerRenderer = new FarmerRenderer(key);
- },
-#else
- // from Bush
- ["TileSheets\\bushes"] = (content, key) => Bush.texture = content.Load<Texture2D>(key),
-
- // from Critter
- ["TileSheets\\critters"] = (content, key) => Critter.critterTexture = content.Load<Texture2D>(key),
-
- // from Farm
- ["Buildings\\houses"] = (content, key) =>
- {
- Farm farm = Game1.getFarm();
- if (farm != null)
- farm.houseTextures = content.Load<Texture2D>(key);
- },
-
- // from Farmer
- ["Characters\\Farmer\\farmer_base"] = (content, key) =>
- {
- if (Game1.player != null && Game1.player.isMale)
- Game1.player.FarmerRenderer = new FarmerRenderer(content.Load<Texture2D>(key));
- },
- ["Characters\\Farmer\\farmer_girl_base"] = (content, key) =>
- {
- if (Game1.player != null && !Game1.player.isMale)
- Game1.player.FarmerRenderer = new FarmerRenderer(content.Load<Texture2D>(key));
- },
-#endif
-
- // from Flooring
- ["TerrainFeatures\\Flooring"] = (content, key) => Flooring.floorsTexture = content.Load<Texture2D>(key),
-
- // from FruitTree
- ["TileSheets\\fruitTrees"] = (content, key) => FruitTree.texture = content.Load<Texture2D>(key),
-
- // from HoeDirt
- ["TerrainFeatures\\hoeDirt"] = (content, key) => HoeDirt.lightTexture = content.Load<Texture2D>(key),
- ["TerrainFeatures\\hoeDirtDark"] = (content, key) => HoeDirt.darkTexture = content.Load<Texture2D>(key),
- ["TerrainFeatures\\hoeDirtSnow"] = (content, key) => HoeDirt.snowTexture = content.Load<Texture2D>(key),
-
- // from TitleMenu
- ["Minigames\\Clouds"] = (content, key) =>
- {
- if (Game1.activeClickableMenu is TitleMenu)
- reflection.GetField<Texture2D>(Game1.activeClickableMenu, "cloudsTexture").SetValue(content.Load<Texture2D>(key));
- },
- ["Minigames\\TitleButtons"] = (content, key) =>
- {
- if (Game1.activeClickableMenu is TitleMenu titleMenu)
- {
- reflection.GetField<Texture2D>(titleMenu, "titleButtonsTexture").SetValue(content.Load<Texture2D>(key));
- foreach (TemporaryAnimatedSprite bird in reflection.GetField<List<TemporaryAnimatedSprite>>(titleMenu, "birds").GetValue())
-#if STARDEW_VALLEY_1_3
- bird.texture = content.Load<Texture2D>(key);
-#else
- bird.Texture = content.Load<Texture2D>(key);
-#endif
- }
- },
-
- // from Wallpaper
- ["Maps\\walls_and_floors"] = (content, key) => Wallpaper.wallpaperTexture = content.Load<Texture2D>(key)
- }
- .ToDictionary(p => getNormalisedPath(p.Key), p => p.Value);
- }
-
- /// <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>
- /// <returns>Returns whether an asset was reloaded.</returns>
- public bool ReloadForKey(LocalizedContentManager content, string key)
- {
- // static assets
- if (this.SingletonSetters.TryGetValue(key, out Action<LocalizedContentManager, string> reload))
- {
- reload(content, key);
- return true;
- }
-
- // building textures
- if (key.StartsWith(this.GetNormalisedPath("Buildings\\")))
- {
- Building[] buildings = this.GetAllBuildings().Where(p => key == this.GetNormalisedPath($"Buildings\\{p.buildingType}")).ToArray();
- if (buildings.Any())
- {
-#if STARDEW_VALLEY_1_3
- foreach (Building building in buildings)
- building.texture = new Lazy<Texture2D>(() => content.Load<Texture2D>(key));
-#else
- Texture2D texture = content.Load<Texture2D>(key);
- foreach (Building building in buildings)
- building.texture = texture;
-#endif
-
- return true;
- }
- return false;
- }
-
- return false;
- }
-
-
- /*********
- ** Private methods
- *********/
- /// <summary>Get all player-constructed buildings in the world.</summary>
- private IEnumerable<Building> GetAllBuildings()
- {
- foreach (BuildableGameLocation location in Game1.locations.OfType<BuildableGameLocation>())
- {
- foreach (Building building in location.buildings)
- yield return building;
- }
- }
- }
-}
diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs
index 4bd40710..1b8cb2ba 100644
--- a/src/SMAPI/Program.cs
+++ b/src/SMAPI/Program.cs
@@ -541,8 +541,10 @@ namespace StardewModdingAPI
this.Monitor.Log("Couldn't check for a new version of SMAPI. This won't affect your game, but you may not be notified of new versions if this keeps happening.", LogLevel.Warn);
this.Monitor.Log($"Error: {response.Error}");
}
- else if (new SemanticVersion(response.Version).IsNewerThan(Constants.ApiVersion))
- this.Monitor.Log($"You can update SMAPI to {response.Version}: {response.Url}", LogLevel.Alert);
+ else if (this.IsValidUpdate(Constants.ApiVersion, new SemanticVersion(response.Version)))
+ this.Monitor.Log($"You can update SMAPI to {response.Version}: {Constants.HomePageUrl}", LogLevel.Alert);
+ else if (response.PreviewVersion != null && this.IsValidUpdate(Constants.ApiVersion, new SemanticVersion(response.PreviewVersion)))
+ this.Monitor.Log($"You can update SMAPI to {response.PreviewVersion}: {Constants.HomePageUrl}", LogLevel.Alert);
else
this.Monitor.Log(" SMAPI okay.", LogLevel.Trace);
}
@@ -656,6 +658,27 @@ namespace StardewModdingAPI
}).Start();
}
+ /// <summary>Get whether a given version should be offered to the user as an update.</summary>
+ /// <param name="currentVersion">The current semantic version.</param>
+ /// <param name="newVersion">The target semantic version.</param>
+ private bool IsValidUpdate(ISemanticVersion currentVersion, ISemanticVersion newVersion)
+ {
+ // basic eligibility
+ bool isNewer = newVersion.IsNewerThan(currentVersion);
+ bool isPrerelease = newVersion.Build != null;
+ bool isEquallyStable = !isPrerelease || currentVersion.Build != null; // don't update stable => prerelease
+ if (!isNewer || !isEquallyStable)
+ return false;
+ if (!isPrerelease)
+ return true;
+
+ // prerelease eligible if same version (excluding prerelease tag)
+ return
+ newVersion.MajorVersion == currentVersion.MajorVersion
+ && newVersion.MinorVersion == currentVersion.MinorVersion
+ && newVersion.PatchVersion == currentVersion.PatchVersion;
+ }
+
/// <summary>Create a directory path if it doesn't exist.</summary>
/// <param name="path">The directory path.</param>
private void VerifyPath(string path)
@@ -930,7 +953,7 @@ namespace StardewModdingAPI
{
helper.ObservableAssetEditors.CollectionChanged += (sender, e) =>
{
- if (e.NewItems.Count > 0)
+ if (e.NewItems?.Count > 0)
{
this.Monitor.Log("Invalidating cache entries for new asset editors...", LogLevel.Trace);
this.ContentCore.InvalidateCacheFor(e.NewItems.Cast<IAssetEditor>().ToArray(), new IAssetLoader[0]);
@@ -938,7 +961,7 @@ namespace StardewModdingAPI
};
helper.ObservableAssetLoaders.CollectionChanged += (sender, e) =>
{
- if (e.NewItems.Count > 0)
+ if (e.NewItems?.Count > 0)
{
this.Monitor.Log("Invalidating cache entries for new asset loaders...", LogLevel.Trace);
this.ContentCore.InvalidateCacheFor(new IAssetEditor[0], e.NewItems.Cast<IAssetLoader>().ToArray());
diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj
index bffb96e2..edddbd2a 100644
--- a/src/SMAPI/StardewModdingAPI.csproj
+++ b/src/SMAPI/StardewModdingAPI.csproj
@@ -66,7 +66,8 @@
<Private>True</Private>
</Reference>
<Reference Include="Newtonsoft.Json, Version=11.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
- <HintPath>..\packages\Newtonsoft.Json.11.0.1-beta3\lib\net45\Newtonsoft.Json.dll</HintPath>
+ <HintPath>..\packages\Newtonsoft.Json.11.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
+ <Private>True</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
@@ -137,7 +138,7 @@
<Compile Include="IReflectedField.cs" />
<Compile Include="IReflectedMethod.cs" />
<Compile Include="IReflectedProperty.cs" />
- <Compile Include="Metadata\CoreAssets.cs" />
+ <Compile Include="Metadata\CoreAssetPropagator.cs" />
<Compile Include="ContentSource.cs" />
<Compile Include="Events\ContentEvents.cs" />
<Compile Include="Events\EventArgsInput.cs" />
diff --git a/src/SMAPI/packages.config b/src/SMAPI/packages.config
index 1a0b78fa..3e876922 100644
--- a/src/SMAPI/packages.config
+++ b/src/SMAPI/packages.config
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Mono.Cecil" version="0.9.6.4" targetFramework="net45" />
- <package id="Newtonsoft.Json" version="11.0.1-beta3" targetFramework="net45" />
+ <package id="Newtonsoft.Json" version="11.0.2" targetFramework="net45" />
</packages> \ No newline at end of file