diff options
Diffstat (limited to 'src/SMAPI')
-rw-r--r-- | src/SMAPI/Constants.cs | 8 | ||||
-rw-r--r-- | src/SMAPI/Framework/Content/AssetDataForImage.cs | 2 | ||||
-rw-r--r-- | src/SMAPI/Framework/ContentCore.cs | 6 | ||||
-rw-r--r-- | src/SMAPI/Metadata/CoreAssetPropagator.cs | 642 | ||||
-rw-r--r-- | src/SMAPI/Metadata/CoreAssets.cs | 220 | ||||
-rw-r--r-- | src/SMAPI/Program.cs | 31 | ||||
-rw-r--r-- | src/SMAPI/StardewModdingAPI.csproj | 5 | ||||
-rw-r--r-- | src/SMAPI/packages.config | 2 |
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 |