diff options
Diffstat (limited to 'src')
31 files changed, 947 insertions, 335 deletions
diff --git a/src/SMAPI.Common/Models/ModInfoModel.cs b/src/SMAPI.Common/Models/ModInfoModel.cs index 48305cb8..48df235a 100644 --- a/src/SMAPI.Common/Models/ModInfoModel.cs +++ b/src/SMAPI.Common/Models/ModInfoModel.cs @@ -9,9 +9,12 @@ namespace StardewModdingAPI.Common.Models /// <summary>The mod name.</summary> public string Name { get; set; } - /// <summary>The mod's semantic version number.</summary> + /// <summary>The semantic version for the mod's latest release.</summary> public string Version { get; set; } + /// <summary>The semantic version for the mod's latest preview release, if available and different from <see cref="Version"/>.</summary> + public string PreviewVersion { get; set; } + /// <summary>The mod's web URL.</summary> public string Url { get; set; } @@ -28,16 +31,17 @@ namespace StardewModdingAPI.Common.Models // needed for JSON deserialising } - /// <summary>Construct an instance.</summary> /// <param name="name">The mod name.</param> - /// <param name="version">The mod's semantic version number.</param> + /// <param name="version">The semantic version for the mod's latest release.</param> + /// <param name="previewVersion">The semantic version for the mod's latest preview release, if available and different from <see cref="Version"/>.</param> /// <param name="url">The mod's web URL.</param> /// <param name="error">The error message indicating why the mod is invalid (if applicable).</param> - public ModInfoModel(string name, string version, string url, string error = null) + public ModInfoModel(string name, string version, string url, string previewVersion = null, string error = null) { this.Name = name; this.Version = version; + this.PreviewVersion = previewVersion; this.Url = url; this.Error = error; // mainly initialised here for the JSON deserialiser } diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets index 7e8bbfc3..d2e37101 100644 --- a/src/SMAPI.ModBuildConfig/build/smapi.targets +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -19,9 +19,14 @@ <!-- set default settings --> <ModFolderName Condition="'$(ModFolderName)' == ''">$(MSBuildProjectName)</ModFolderName> + <ModUnitTests Condition="'$(ModUnitTests)' == ''">False</ModUnitTests> <ModZipPath Condition="'$(ModZipPath)' == ''">$(TargetDir)</ModZipPath> <EnableModDeploy Condition="'$(EnableModDeploy)' == ''">True</EnableModDeploy> <EnableModZip Condition="'$(EnableModZip)' == ''">True</EnableModZip> + + <!-- disable mod deploy in unit test project --> + <EnableModDeploy Condition="'$(ModUnitTests)' == true">False</EnableModDeploy> + <EnableModZip Condition="'$(ModUnitTests)' == true">False</EnableModZip> </PropertyGroup> <!-- find platform + game path --> @@ -57,32 +62,40 @@ <ItemGroup> <Reference Include="Microsoft.Xna.Framework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86"> <Private>false</Private> + <Private Condition="$(ModUnitTests)">true</Private> </Reference> <Reference Include="Microsoft.Xna.Framework.Game, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86"> <Private>false</Private> + <Private Condition="$(ModUnitTests)">true</Private> </Reference> <Reference Include="Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86"> <Private>false</Private> + <Private Condition="$(ModUnitTests)">true</Private> </Reference> <Reference Include="Microsoft.Xna.Framework.Xact, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553, processorArchitecture=x86"> <Private>false</Private> + <Private Condition="$(ModUnitTests)">true</Private> </Reference> <Reference Include="Netcode" Condition="Exists('$(GamePath)\Netcode.dll')"> <HintPath>$(GamePath)\Netcode.dll</HintPath> <Private>False</Private> + <Private Condition="$(ModUnitTests)">true</Private> </Reference> <Reference Include="Stardew Valley"> <HintPath>$(GamePath)\Stardew Valley.exe</HintPath> <Private>false</Private> + <Private Condition="$(ModUnitTests)">true</Private> </Reference> <Reference Include="StardewModdingAPI"> <HintPath>$(GamePath)\StardewModdingAPI.exe</HintPath> <Private>false</Private> + <Private Condition="$(ModUnitTests)">true</Private> </Reference> <Reference Include="xTile, Version=2.0.4.0, Culture=neutral, processorArchitecture=x86"> <HintPath>$(GamePath)\xTile.dll</HintPath> <Private>false</Private> <SpecificVersion>False</SpecificVersion> + <Private Condition="$(ModUnitTests)">true</Private> </Reference> </ItemGroup> @@ -100,18 +113,22 @@ <HintPath>$(GamePath)\MonoGame.Framework.dll</HintPath> <Private>false</Private> <SpecificVersion>False</SpecificVersion> + <Private Condition="$(ModUnitTests)">true</Private> </Reference> <Reference Include="StardewValley"> <HintPath>$(GamePath)\StardewValley.exe</HintPath> <Private>false</Private> + <Private Condition="$(ModUnitTests)">true</Private> </Reference> <Reference Include="StardewModdingAPI"> <HintPath>$(GamePath)\StardewModdingAPI.exe</HintPath> <Private>false</Private> + <Private Condition="$(ModUnitTests)">true</Private> </Reference> <Reference Include="xTile"> <HintPath>$(GamePath)\xTile.dll</HintPath> <Private>false</Private> + <Private Condition="$(ModUnitTests)">true</Private> </Reference> </ItemGroup> </Otherwise> diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec index 8393ab61..d24e15be 100644 --- a/src/SMAPI.ModBuildConfig/package.nuspec +++ b/src/SMAPI.ModBuildConfig/package.nuspec @@ -2,7 +2,7 @@ <package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd"> <metadata> <id>Pathoschild.Stardew.ModBuildConfig</id> - <version>2.0.3-alpha20180307</version> + <version>2.0.3-alpha20180325</version> <title>Build package for SMAPI mods</title> <authors>Pathoschild</authors> <owners>Pathoschild</owners> @@ -29,6 +29,7 @@ 2.0.3: - Added support for Stardew Valley 1.3. + - Added support for unit test projects. </releaseNotes> </metadata> <files> diff --git a/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj b/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj index a5b89a33..d1f72c6c 100644 --- a/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj +++ b/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj @@ -37,8 +37,8 @@ </PropertyGroup> <ItemGroup> <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> - <Private>False</Private> + <HintPath>..\packages\Newtonsoft.Json.11.0.2\lib\net45\Newtonsoft.Json.dll</HintPath> + <Private>True</Private> </Reference> <Reference Include="System" /> <Reference Include="System.Xml" /> diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index 785af01a..a56cf66d 100644 --- a/src/SMAPI.Mods.ConsoleCommands/manifest.json +++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json @@ -1,7 +1,7 @@ { "Name": "Console Commands", "Author": "SMAPI", - "Version": "2.5.3", + "Version": "2.5.4", "Description": "Adds SMAPI console commands that let you manipulate the game.", "UniqueID": "SMAPI.ConsoleCommands", "EntryDll": "ConsoleCommands.dll" diff --git a/src/SMAPI.Mods.ConsoleCommands/packages.config b/src/SMAPI.Mods.ConsoleCommands/packages.config index a0f76c34..c8b3ae63 100644 --- a/src/SMAPI.Mods.ConsoleCommands/packages.config +++ b/src/SMAPI.Mods.ConsoleCommands/packages.config @@ -1,4 +1,4 @@ <?xml version="1.0" encoding="utf-8"?> <packages> - <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 diff --git a/src/SMAPI.Web/Controllers/IndexController.cs b/src/SMAPI.Web/Controllers/IndexController.cs index 5d45118f..0464e50a 100644 --- a/src/SMAPI.Web/Controllers/IndexController.cs +++ b/src/SMAPI.Web/Controllers/IndexController.cs @@ -3,6 +3,7 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; +using StardewModdingAPI.Common; using StardewModdingAPI.Web.Framework.Clients.GitHub; using StardewModdingAPI.Web.ViewModels; @@ -23,7 +24,10 @@ namespace StardewModdingAPI.Web.Controllers private readonly IGitHubClient GitHub; /// <summary>The cache time for release info.</summary> - private readonly TimeSpan CacheTime = TimeSpan.FromMinutes(5); + private readonly TimeSpan CacheTime = TimeSpan.FromSeconds(1); + + /// <summary>The GitHub repository name to check for update.</summary> + private readonly string RepositoryName = "Pathoschild/SMAPI"; /********* @@ -42,17 +46,24 @@ namespace StardewModdingAPI.Web.Controllers [HttpGet] public async Task<ViewResult> Index() { - // fetch latest SMAPI release - GitRelease release = await this.Cache.GetOrCreateAsync("latest-smapi-release", async entry => + // fetch SMAPI releases + IndexVersionModel stableVersion = await this.Cache.GetOrCreateAsync("stable-version", async entry => + { + entry.AbsoluteExpiration = DateTimeOffset.UtcNow.Add(this.CacheTime); + GitRelease release = await this.GitHub.GetLatestReleaseAsync(this.RepositoryName, includePrerelease: false); + return new IndexVersionModel(release.Name, release.Body, this.GetMainDownloadUrl(release), this.GetDevDownloadUrl(release)); + }); + IndexVersionModel betaVersion = await this.Cache.GetOrCreateAsync("beta-version", async entry => { entry.AbsoluteExpiration = DateTimeOffset.UtcNow.Add(this.CacheTime); - return await this.GitHub.GetLatestReleaseAsync("Pathoschild/SMAPI"); + GitRelease release = await this.GitHub.GetLatestReleaseAsync(this.RepositoryName, includePrerelease: true); + return release.IsPrerelease + ? this.GetBetaDownload(release) + : null; }); - string downloadUrl = this.GetMainDownloadUrl(release); - string devDownloadUrl = this.GetDevDownloadUrl(release); // render view - var model = new IndexModel(release.Name, release.Body, downloadUrl, devDownloadUrl); + var model = new IndexModel(stableVersion, betaVersion); return this.View(model); } @@ -89,5 +100,33 @@ namespace StardewModdingAPI.Web.Controllers // fallback just in case return "https://github.com/pathoschild/SMAPI/releases"; } + + /// <summary>Get the latest beta download for a SMAPI release.</summary> + /// <param name="release">The SMAPI release.</param> + private IndexVersionModel GetBetaDownload(GitRelease release) + { + // get download with the latest version + SemanticVersionImpl latestVersion = null; + string latestUrl = null; + foreach (GitAsset asset in release.Assets ?? new GitAsset[0]) + { + // parse version + Match versionMatch = Regex.Match(asset.FileName, @"SMAPI-([\d\.]+(?:-.+)?)-installer.zip"); + if (!versionMatch.Success || !SemanticVersionImpl.TryParse(versionMatch.Groups[1].Value, out SemanticVersionImpl version)) + continue; + + // save latest version + if (latestVersion == null || latestVersion.CompareTo(version) < 0) + { + latestVersion = version; + latestUrl = asset.DownloadUrl; + } + } + + // return if prerelease + return latestVersion?.Tag != null + ? new IndexVersionModel(latestVersion.ToString(), release.Body, latestUrl, null) + : null; + } } } diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index abae7db7..24517263 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -29,8 +29,11 @@ namespace StardewModdingAPI.Web.Controllers /// <summary>The cache in which to store mod metadata.</summary> private readonly IMemoryCache Cache; - /// <summary>The number of minutes update checks should be cached before refetching them.</summary> - private readonly int CacheMinutes; + /// <summary>The number of minutes successful update checks should be cached before refetching them.</summary> + private readonly int SuccessCacheMinutes; + + /// <summary>The number of minutes failed update checks should be cached before refetching them.</summary> + private readonly int ErrorCacheMinutes; /// <summary>A regex which matches SMAPI-style semantic version.</summary> private readonly string VersionRegex; @@ -50,7 +53,8 @@ namespace StardewModdingAPI.Web.Controllers ModUpdateCheckConfig config = configProvider.Value; this.Cache = cache; - this.CacheMinutes = config.CacheMinutes; + this.SuccessCacheMinutes = config.SuccessCacheMinutes; + this.ErrorCacheMinutes = config.ErrorCacheMinutes; this.VersionRegex = config.SemanticVersionRegex; this.Repositories = new IModRepository[] @@ -115,13 +119,13 @@ namespace StardewModdingAPI.Web.Controllers if (info.Error == null) { if (info.Version == null) - info = new ModInfoModel(info.Name, info.Version, info.Url, "Mod has no version number."); + info = new ModInfoModel(name: info.Name, version: info.Version, url: info.Url, error: "Mod has no version number."); if (!allowInvalidVersions && !Regex.IsMatch(info.Version, this.VersionRegex, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)) - info = new ModInfoModel(info.Name, info.Version, info.Url, $"Mod has invalid semantic version '{info.Version}'."); + info = new ModInfoModel(name: info.Name, version: info.Version, url: info.Url, error: $"Mod has invalid semantic version '{info.Version}'."); } // cache & return - entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(this.CacheMinutes); + entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(info.Error == null ? this.SuccessCacheMinutes : this.ErrorCacheMinutes); return info; }); } diff --git a/src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs b/src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs index 0b205660..4abe0737 100644 --- a/src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs +++ b/src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Net; using System.Threading.Tasks; using Pathoschild.Http.Client; @@ -11,8 +12,11 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub /********* ** Properties *********/ - /// <summary>The URL for a GitHub releases API query excluding the base URL, where {0} is the repository owner and name.</summary> - private readonly string ReleaseUrlFormat; + /// <summary>The URL for a GitHub API query for the latest stable release, excluding the base URL, where {0} is the organisation and project name.</summary> + private readonly string StableReleaseUrlFormat; + + /// <summary>The URL for a GitHub API query for the latest release (including prerelease), excluding the base URL, where {0} is the organisation and project name.</summary> + private readonly string AnyReleaseUrlFormat; /// <summary>The underlying HTTP client.</summary> private readonly IClient Client; @@ -23,14 +27,16 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub *********/ /// <summary>Construct an instance.</summary> /// <param name="baseUrl">The base URL for the GitHub API.</param> - /// <param name="releaseUrlFormat">The URL for a GitHub releases API query excluding the <paramref name="baseUrl"/>, where {0} is the repository owner and name.</param> + /// <param name="stableReleaseUrlFormat">The URL for a GitHub API query for the latest stable release, excluding the <paramref name="baseUrl"/>, where {0} is the organisation and project name.</param> + /// <param name="anyReleaseUrlFormat">The URL for a GitHub API query for the latest release (including prerelease), excluding the <paramref name="baseUrl"/>, where {0} is the organisation and project name.</param> /// <param name="userAgent">The user agent for the API client.</param> /// <param name="acceptHeader">The Accept header value expected by the GitHub API.</param> /// <param name="username">The username with which to authenticate to the GitHub API.</param> /// <param name="password">The password with which to authenticate to the GitHub API.</param> - public GitHubClient(string baseUrl, string releaseUrlFormat, string userAgent, string acceptHeader, string username, string password) + public GitHubClient(string baseUrl, string stableReleaseUrlFormat, string anyReleaseUrlFormat, string userAgent, string acceptHeader, string username, string password) { - this.ReleaseUrlFormat = releaseUrlFormat; + this.StableReleaseUrlFormat = stableReleaseUrlFormat; + this.AnyReleaseUrlFormat = anyReleaseUrlFormat; this.Client = new FluentClient(baseUrl) .SetUserAgent(userAgent) @@ -41,18 +47,23 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub /// <summary>Get the latest release for a GitHub repository.</summary> /// <param name="repo">The repository key (like <c>Pathoschild/SMAPI</c>).</param> - /// <returns>Returns the latest release if found, else <c>null</c>.</returns> - public async Task<GitRelease> GetLatestReleaseAsync(string repo) + /// <param name="includePrerelease">Whether to return a prerelease version if it's latest.</param> + /// <returns>Returns the release if found, else <c>null</c>.</returns> + public async Task<GitRelease> GetLatestReleaseAsync(string repo, bool includePrerelease = false) { - // validate key format - if (!repo.Contains("/") || repo.IndexOf("/", StringComparison.InvariantCultureIgnoreCase) != repo.LastIndexOf("/", StringComparison.InvariantCultureIgnoreCase)) - throw new ArgumentException($"The value '{repo}' isn't a valid GitHub repository key, must be a username and project name like 'Pathoschild/SMAPI'.", nameof(repo)); - - // fetch info + this.AssetKeyFormat(repo); try { + if (includePrerelease) + { + GitRelease[] results = await this.Client + .GetAsync(string.Format(this.AnyReleaseUrlFormat, repo)) + .AsArray<GitRelease>(); + return results.FirstOrDefault(); + } + return await this.Client - .GetAsync(string.Format(this.ReleaseUrlFormat, repo)) + .GetAsync(string.Format(this.StableReleaseUrlFormat, repo)) .As<GitRelease>(); } catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound) @@ -66,5 +77,18 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub { this.Client?.Dispose(); } + + + /********* + ** Private methods + *********/ + /// <summary>Assert that a repository key is formatted correctly.</summary> + /// <param name="repo">The repository key (like <c>Pathoschild/SMAPI</c>).</param> + /// <exception cref="ArgumentException">The repository key is invalid.</exception> + private void AssetKeyFormat(string repo) + { + if (repo == null || !repo.Contains("/") || repo.IndexOf("/", StringComparison.InvariantCultureIgnoreCase) != repo.LastIndexOf("/", StringComparison.InvariantCultureIgnoreCase)) + throw new ArgumentException($"The value '{repo}' isn't a valid GitHub repository key, must be a username and project name like 'Pathoschild/SMAPI'.", nameof(repo)); + } } } diff --git a/src/SMAPI.Web/Framework/Clients/GitHub/GitRelease.cs b/src/SMAPI.Web/Framework/Clients/GitHub/GitRelease.cs index b944088d..827374fb 100644 --- a/src/SMAPI.Web/Framework/Clients/GitHub/GitRelease.cs +++ b/src/SMAPI.Web/Framework/Clients/GitHub/GitRelease.cs @@ -19,6 +19,10 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub /// <summary>The Markdown description for the release.</summary> public string Body { get; set; } + /// <summary>Whether this is a prerelease version.</summary> + [JsonProperty("prerelease")] + public bool IsPrerelease { get; set; } + /// <summary>The attached files.</summary> public GitAsset[] Assets { get; set; } } diff --git a/src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs b/src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs index 6e8eadff..9519c26f 100644 --- a/src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs +++ b/src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs @@ -11,7 +11,8 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub *********/ /// <summary>Get the latest release for a GitHub repository.</summary> /// <param name="repo">The repository key (like <c>Pathoschild/SMAPI</c>).</param> - /// <returns>Returns the latest release if found, else <c>null</c>.</returns> - Task<GitRelease> GetLatestReleaseAsync(string repo); + /// <param name="includePrerelease">Whether to return a prerelease version if it's latest.</param> + /// <returns>Returns the release if found, else <c>null</c>.</returns> + Task<GitRelease> GetLatestReleaseAsync(string repo, bool includePrerelease = false); } } diff --git a/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs index 61219414..de6c024a 100644 --- a/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs +++ b/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs @@ -29,8 +29,11 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels /// <summary>The base URL for the GitHub API.</summary> public string GitHubBaseUrl { get; set; } - /// <summary>The URL for a GitHub API latest-release query excluding the <see cref="GitHubBaseUrl"/>, where {0} is the organisation and project name.</summary> - public string GitHubReleaseUrlFormat { get; set; } + /// <summary>The URL for a GitHub API query for the latest stable release, excluding the <see cref="GitHubBaseUrl"/>, where {0} is the organisation and project name.</summary> + public string GitHubStableReleaseUrlFormat { get; set; } + + /// <summary>The URL for a GitHub API query for the latest release (including prerelease), excluding the <see cref="GitHubBaseUrl"/>, where {0} is the organisation and project name.</summary> + public string GitHubAnyReleaseUrlFormat { get; set; } /// <summary>The Accept header value expected by the GitHub API.</summary> public string GitHubAcceptHeader { get; set; } diff --git a/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs index 58c3a100..fc3b7dc2 100644 --- a/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs +++ b/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs @@ -6,8 +6,11 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels /********* ** Accessors *********/ - /// <summary>The number of minutes update checks should be cached before refetching them.</summary> - public int CacheMinutes { get; set; } + /// <summary>The number of minutes successful update checks should be cached before refetching them.</summary> + public int SuccessCacheMinutes { get; set; } + + /// <summary>The number of minutes failed update checks should be cached before refetching them.</summary> + public int ErrorCacheMinutes { get; set; } /// <summary>A regex which matches SMAPI-style semantic version.</summary> /// <remarks>Derived from SMAPI's SemanticVersion implementation.</remarks> diff --git a/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs index 266055a6..3e5a4272 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs @@ -43,7 +43,7 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories return new ModInfoModel("Found no mod with this ID."); // create model - return new ModInfoModel(mod.Name, this.NormaliseVersion(mod.Version), mod.Url); + return new ModInfoModel(name: mod.Name, version: this.NormaliseVersion(mod.Version), url: mod.Url); } catch (Exception ex) { diff --git a/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs index 7bad6127..59eb8cd1 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs @@ -38,10 +38,21 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories // fetch info try { - GitRelease release = await this.Client.GetLatestReleaseAsync(id); - return release != null - ? new ModInfoModel(id, this.NormaliseVersion(release.Tag), $"https://github.com/{id}/releases") - : new ModInfoModel("Found no mod with this ID."); + // get latest release + GitRelease latest = await this.Client.GetLatestReleaseAsync(id, includePrerelease: true); + GitRelease preview = null; + if (latest == null) + return new ModInfoModel("Found no mod with this ID."); + + // get latest stable release (if not latest) + if (latest.IsPrerelease) + { + preview = latest; + latest = await this.Client.GetLatestReleaseAsync(id, includePrerelease: false); + } + + // return data + return new ModInfoModel(name: id, version: this.NormaliseVersion(latest?.Tag), previewVersion: this.NormaliseVersion(preview?.Tag), url: $"https://github.com/{id}/releases"); } catch (Exception ex) { diff --git a/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs index e1dc0fcc..6411ad4c 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs @@ -43,7 +43,7 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories return new ModInfoModel("Found no mod with this ID."); if (mod.Error != null) return new ModInfoModel(mod.Error); - return new ModInfoModel(mod.Name, this.NormaliseVersion(mod.Version), mod.Url); + return new ModInfoModel(name: mod.Name, version: this.NormaliseVersion(mod.Version), url: mod.Url); } catch (Exception ex) { diff --git a/src/SMAPI.Web/StardewModdingAPI.Web.csproj b/src/SMAPI.Web/StardewModdingAPI.Web.csproj index 19198503..e2eee8a8 100644 --- a/src/SMAPI.Web/StardewModdingAPI.Web.csproj +++ b/src/SMAPI.Web/StardewModdingAPI.Web.csproj @@ -10,13 +10,13 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="HtmlAgilityPack" Version="1.6.0" /> - <PackageReference Include="Markdig" Version="0.14.8" /> - <PackageReference Include="Microsoft.AspNetCore" Version="2.0.0" /> - <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.0.0" /> - <PackageReference Include="Microsoft.AspNetCore.Rewrite" Version="2.0.0" /> - <PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.0.0" /> - <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="2.0.0" /> + <PackageReference Include="HtmlAgilityPack" Version="1.7.2" /> + <PackageReference Include="Markdig" Version="0.14.9" /> + <PackageReference Include="Microsoft.AspNetCore" Version="2.0.2" /> + <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.0.3" /> + <PackageReference Include="Microsoft.AspNetCore.Rewrite" Version="2.0.2" /> + <PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.0.2" /> + <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="2.0.1" /> <PackageReference Include="Pathoschild.Http.FluentClient" Version="3.1.0" /> </ItemGroup> <ItemGroup> diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index d7d4d074..47102e5c 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -74,7 +74,8 @@ namespace StardewModdingAPI.Web services.AddSingleton<IGitHubClient>(new GitHubClient( baseUrl: api.GitHubBaseUrl, - releaseUrlFormat: api.GitHubReleaseUrlFormat, + stableReleaseUrlFormat: api.GitHubStableReleaseUrlFormat, + anyReleaseUrlFormat: api.GitHubAnyReleaseUrlFormat, userAgent: userAgent, acceptHeader: api.GitHubAcceptHeader, username: api.GitHubUsername, diff --git a/src/SMAPI.Web/ViewModels/IndexModel.cs b/src/SMAPI.Web/ViewModels/IndexModel.cs index 6d3da91e..4268c878 100644 --- a/src/SMAPI.Web/ViewModels/IndexModel.cs +++ b/src/SMAPI.Web/ViewModels/IndexModel.cs @@ -6,17 +6,11 @@ namespace StardewModdingAPI.Web.ViewModels /********* ** Accessors *********/ - /// <summary>The latest SMAPI version.</summary> - public string LatestVersion { get; set; } + /// <summary>The latest stable SMAPI version.</summary> + public IndexVersionModel StableVersion { get; set; } - /// <summary>The Markdown description for the release.</summary> - public string Description { get; set; } - - /// <summary>The main download URL.</summary> - public string DownloadUrl { get; set; } - - /// <summary>The for-developers download URL.</summary> - public string DevDownloadUrl { get; set; } + /// <summary>The latest prerelease SMAPI version (if newer than <see cref="StableVersion"/>).</summary> + public IndexVersionModel BetaVersion { get; set; } /********* @@ -26,16 +20,12 @@ namespace StardewModdingAPI.Web.ViewModels public IndexModel() { } /// <summary>Construct an instance.</summary> - /// <param name="latestVersion">The latest SMAPI version.</param> - /// <param name="description">The Markdown description for the release.</param> - /// <param name="downloadUrl">The main download URL.</param> - /// <param name="devDownloadUrl">The for-developers download URL.</param> - internal IndexModel(string latestVersion, string description, string downloadUrl, string devDownloadUrl) + /// <param name="stableVersion">The latest stable SMAPI version.</param> + /// <param name="betaVersion">The latest prerelease SMAPI version (if newer than <paramref name="stableVersion"/>).</param> + internal IndexModel(IndexVersionModel stableVersion, IndexVersionModel betaVersion) { - this.LatestVersion = latestVersion; - this.Description = description; - this.DownloadUrl = downloadUrl; - this.DevDownloadUrl = devDownloadUrl; + this.StableVersion = stableVersion; + this.BetaVersion = betaVersion; } } } diff --git a/src/SMAPI.Web/ViewModels/IndexVersionModel.cs b/src/SMAPI.Web/ViewModels/IndexVersionModel.cs new file mode 100644 index 00000000..4f63b979 --- /dev/null +++ b/src/SMAPI.Web/ViewModels/IndexVersionModel.cs @@ -0,0 +1,41 @@ +namespace StardewModdingAPI.Web.ViewModels +{ + /// <summary>The fields for a SMAPI version.</summary> + public class IndexVersionModel + { + /********* + ** Accessors + *********/ + /// <summary>The release version.</summary> + public string Version { get; set; } + + /// <summary>The Markdown description for the release.</summary> + public string Description { get; set; } + + /// <summary>The main download URL.</summary> + public string DownloadUrl { get; set; } + + /// <summary>The for-developers download URL (not applicable for prerelease versions).</summary> + public string DevDownloadUrl { get; set; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + public IndexVersionModel() { } + + /// <summary>Construct an instance.</summary> + /// <param name="version">The release number.</param> + /// <param name="description">The Markdown description for the release.</param> + /// <param name="downloadUrl">The main download URL.</param> + /// <param name="devDownloadUrl">The for-developers download URL (not applicable for prerelease versions).</param> + internal IndexVersionModel(string version, string description, string downloadUrl, string devDownloadUrl) + { + this.Version = version; + this.Description = description; + this.DownloadUrl = downloadUrl; + this.DevDownloadUrl = devDownloadUrl; + } + } +} diff --git a/src/SMAPI.Web/Views/Index/Index.cshtml b/src/SMAPI.Web/Views/Index/Index.cshtml index ad58898e..4efb9f8a 100644 --- a/src/SMAPI.Web/Views/Index/Index.cshtml +++ b/src/SMAPI.Web/Views/Index/Index.cshtml @@ -13,7 +13,11 @@ </p> <div id="call-to-action"> - <a href="@Model.DownloadUrl" class="main-cta">Download SMAPI @Model.LatestVersion</a><br /> + <a href="@Model.StableVersion.DownloadUrl" class="main-cta">Download SMAPI @Model.StableVersion.Version</a><br /> + @if (Model.BetaVersion != null) + { + <a href="@Model.BetaVersion.DownloadUrl" class="secondary-cta">Download SMAPI @Model.BetaVersion.Version<br /><small>for Stardew Valley 1.3 beta</small></a><br /> + } <a href="https://stardewvalleywiki.com/Modding:Installing_SMAPI" class="secondary-cta">Install guide</a><br /> <a href="https://stardewvalleywiki.com/Modding:Player_FAQs" class="secondary-cta">FAQs</a><br /> <img src="favicon.ico" /> @@ -25,12 +29,29 @@ <li>Get help <a href="https://stardewvalleywiki.com/Modding:Community#Discord">on Discord</a> or <a href="https://community.playstarbound.com/threads/smapi-stardew-modding-api.108375/">in the forums</a></li> </ul> -<h2>What's new in SMAPI @Model.LatestVersion?</h2> -<div class="github-description"> - @Html.Raw(Markdig.Markdown.ToHtml(Model.Description)) -</div> +@if (Model.BetaVersion == null) +{ + <h2>What's new in SMAPI @Model.StableVersion.Version?</h2> + <div class="github-description"> + @Html.Raw(Markdig.Markdown.ToHtml(Model.StableVersion.Description)) + </div> + <p>See the <a href="https://github.com/Pathoschild/SMAPI/blob/develop/docs/release-notes.md#release-notes">release notes</a> and <a href="https://stardewvalleywiki.com/Modding:SMAPI_compatibility">mod compatibility list</a> for more info.</p> +} +else +{ + <h2>What's new in...</h2> + <h3>SMAPI @Model.StableVersion.Version?</h3> + <div class="github-description"> + @Html.Raw(Markdig.Markdown.ToHtml(Model.StableVersion.Description)) + </div> + <p>See the <a href="https://github.com/Pathoschild/SMAPI/blob/develop/docs/release-notes.md#release-notes">release notes</a> and <a href="https://stardewvalleywiki.com/Modding:SMAPI_compatibility">mod compatibility list</a> for more info.</p> -<p>See the <a href="https://github.com/Pathoschild/SMAPI/blob/develop/docs/release-notes.md#release-notes">release notes</a> and <a href="https://stardewvalleywiki.com/Modding:SMAPI_compatibility">mod compatibility list</a> for more info.</p> + <h3>SMAPI @Model.BetaVersion.Version?</h3> + <div class="github-description"> + @Html.Raw(Markdig.Markdown.ToHtml(Model.BetaVersion.Description)) + </div> + <p>See the <a href="https://github.com/Pathoschild/SMAPI/blob/develop/docs/release-notes.md#release-notes">release notes</a> and <a href="https://stardewvalleywiki.com/Modding:SMAPI_compatibility">mod compatibility list</a> for more info.</p> +} <h2>Donate to support SMAPI ♥</h2> <p> @@ -62,7 +83,7 @@ <h2>For mod creators</h2> <ul> - <li><a href="@Model.DevDownloadUrl">SMAPI @Model.LatestVersion for developers</a> (includes <a href="https://docs.microsoft.com/en-us/visualstudio/ide/using-intellisense">intellisense</a> and full console output)</li> + <li><a href="@Model.StableVersion.DevDownloadUrl">SMAPI @Model.StableVersion.Version for developers</a> (includes <a href="https://docs.microsoft.com/en-us/visualstudio/ide/using-intellisense">intellisense</a> and full console output)</li> <li><a href="https://stardewvalleywiki.com/Modding:Index">Modding documentation</a></li> <li>Need help? Come <a href="https://stardewvalleywiki.com/Modding:Community#Discord">chat on Discord</a>.</li> </ul> diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index d2d8004e..7213e286 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -54,19 +54,19 @@ <caption>Game info:</caption> <tr> <td>SMAPI version:</td> - <td>@Model.ParsedLog.ApiVersion</td> + <td v-pre>@Model.ParsedLog.ApiVersion</td> </tr> <tr> <td>Game version:</td> - <td>@Model.ParsedLog.GameVersion</td> + <td v-pre>@Model.ParsedLog.GameVersion</td> </tr> <tr> <td>Platform:</td> - <td>@Model.ParsedLog.OperatingSystem</td> + <td v-pre>@Model.ParsedLog.OperatingSystem</td> </tr> <tr> <td>Mods path:</td> - <td>@Model.ParsedLog.ModPath</td> + <td v-pre>@Model.ParsedLog.ModPath</td> </tr> <tr> <td>Log started:</td> @@ -85,7 +85,7 @@ { <tr v-on:click="toggleMod('@GetSlug(mod.Name)')" class="mod-entry" v-bind:class="{ hidden: !showMods['@GetSlug(mod.Name)'] }"> <td><input type="checkbox" v-bind:checked="showMods['@GetSlug(mod.Name)']" v-show="anyModsHidden" /></td> - <td> + <td v-pre> @mod.Name @if (contentPacks != null && contentPacks.TryGetValue(mod.Name, out LogModInfo[] contentPackList)) { @@ -97,19 +97,19 @@ </div> } </td> - <td>@mod.Version</td> - <td>@mod.Author</td> + <td v-pre>@mod.Version</td> + <td v-pre>@mod.Author</td> @if (mod.Errors == 0) { - <td class="color-green">no errors</td> + <td v-pre class="color-green">no errors</td> } else if (mod.Errors == 1) { - <td class="color-red">@mod.Errors error</td> + <td v-pre class="color-red">@mod.Errors error</td> } else { - <td class="color-red">@mod.Errors errors</td> + <td v-pre class="color-red">@mod.Errors errors</td> } </tr> } @@ -130,16 +130,16 @@ string levelStr = message.Level.ToString().ToLower(); <tr class="@levelStr mod" v-show="filtersAllow('@GetSlug(message.Mod)', '@levelStr')"> - <td>@message.Time</td> - <td>@message.Level.ToString().ToUpper()</td> - <td data-title="@message.Mod">@message.Mod</td> - <td>@message.Text</td> + <td v-pre>@message.Time</td> + <td v-pre>@message.Level.ToString().ToUpper()</td> + <td v-pre data-title="@message.Mod">@message.Mod</td> + <td v-pre>@message.Text</td> </tr> if (message.Repeated > 0) { <tr class="@levelStr mod mod-repeat" v-show="filtersAllow('@GetSlug(message.Mod)', '@levelStr')"> <td colspan="3"></td> - <td><i>repeats [@message.Repeated] times.</i></td> + <td v-pre><i>repeats [@message.Repeated] times.</i></td> </tr> } } @@ -151,11 +151,11 @@ else if (Model.ParsedLog?.IsValid == false) <h2>Parsed log</h2> <div id="error" class="color-red"> <p><strong>We couldn't parse that file, but you can still share the link.</strong></p> - <p>Error details: @Model.ParsedLog.Error</p> + <p v-pre>Error details: @Model.ParsedLog.Error</p> </div> <h3>Raw log</h3> - <pre>@Model.ParsedLog.RawText</pre> + <pre v-pre>@Model.ParsedLog.RawText</pre> } <div id="upload-area"> diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json index 3cf72ddb..03ca31ed 100644 --- a/src/SMAPI.Web/appsettings.json +++ b/src/SMAPI.Web/appsettings.json @@ -24,7 +24,8 @@ "ChucklefishModPageUrlFormat": "resources/{0}", "GitHubBaseUrl": "https://api.github.com", - "GitHubReleaseUrlFormat": "repos/{0}/releases/latest", + "GitHubStableReleaseUrlFormat": "repos/{0}/releases/latest", + "GitHubAnyReleaseUrlFormat": "repos/{0}/releases?per_page=1", "GitHubAcceptHeader": "application/vnd.github.v3+json", "GitHubUsername": null, // see top note "GitHubPassword": null, // see top note @@ -39,7 +40,8 @@ }, "ModUpdateCheck": { - "CacheMinutes": 60, + "SuccessCacheMinutes": 60, + "ErrorCacheMinutes": 5, "SemanticVersionRegex": "^(?>(?<major>0|[1-9]\\d*))\\.(?>(?<minor>0|[1-9]\\d*))(?>(?:\\.(?<patch>0|[1-9]\\d*))?)(?:-(?<prerelease>(?>[a-z0-9]+[\\-\\.]?)+))?$", "ChucklefishKey": "Chucklefish", 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 |