From 9e052ae91671024c9c6b74754ec9d184a57a5278 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 13 Mar 2018 20:36:25 -0400 Subject: [PATCH 01/29] hide SMAPI 2.6 release notes to avoid confusion --- docs/release-notes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index fdc06c872..01048b4c0 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,4 +1,5 @@ # Release notes + ## 2.5.3 * For players: From 436c071ba4ecbe43769b438277d441057d05403e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 15 Mar 2018 19:52:18 -0400 Subject: [PATCH 02/29] add support for preview GitHub releases (#457) --- src/SMAPI.Common/Models/ModInfoModel.cs | 12 +++-- .../Framework/Clients/GitHub/GitHubClient.cs | 50 ++++++++++++++----- .../Framework/Clients/GitHub/GitRelease.cs | 4 ++ .../Framework/Clients/GitHub/IGitHubClient.cs | 5 +- .../ConfigModels/ApiClientsConfig.cs | 7 ++- .../ModRepositories/ChucklefishRepository.cs | 2 +- .../ModRepositories/GitHubRepository.cs | 19 +++++-- .../ModRepositories/NexusRepository.cs | 2 +- src/SMAPI.Web/Startup.cs | 3 +- src/SMAPI.Web/appsettings.json | 3 +- 10 files changed, 78 insertions(+), 29 deletions(-) diff --git a/src/SMAPI.Common/Models/ModInfoModel.cs b/src/SMAPI.Common/Models/ModInfoModel.cs index 48305cb88..48df235a0 100644 --- a/src/SMAPI.Common/Models/ModInfoModel.cs +++ b/src/SMAPI.Common/Models/ModInfoModel.cs @@ -9,9 +9,12 @@ internal class ModInfoModel /// The mod name. public string Name { get; set; } - /// The mod's semantic version number. + /// The semantic version for the mod's latest release. public string Version { get; set; } + /// The semantic version for the mod's latest preview release, if available and different from . + public string PreviewVersion { get; set; } + /// The mod's web URL. public string Url { get; set; } @@ -28,16 +31,17 @@ public ModInfoModel() // needed for JSON deserialising } - /// Construct an instance. /// The mod name. - /// The mod's semantic version number. + /// The semantic version for the mod's latest release. + /// The semantic version for the mod's latest preview release, if available and different from . /// The mod's web URL. /// The error message indicating why the mod is invalid (if applicable). - 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.Web/Framework/Clients/GitHub/GitHubClient.cs b/src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs index 0b2056609..4abe07375 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 @@ internal class GitHubClient : IGitHubClient /********* ** Properties *********/ - /// The URL for a GitHub releases API query excluding the base URL, where {0} is the repository owner and name. - private readonly string ReleaseUrlFormat; + /// The URL for a GitHub API query for the latest stable release, excluding the base URL, where {0} is the organisation and project name. + private readonly string StableReleaseUrlFormat; + + /// 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. + private readonly string AnyReleaseUrlFormat; /// The underlying HTTP client. private readonly IClient Client; @@ -23,14 +27,16 @@ internal class GitHubClient : IGitHubClient *********/ /// Construct an instance. /// The base URL for the GitHub API. - /// The URL for a GitHub releases API query excluding the , where {0} is the repository owner and name. + /// The URL for a GitHub API query for the latest stable release, excluding the , where {0} is the organisation and project name. + /// The URL for a GitHub API query for the latest release (including prerelease), excluding the , where {0} is the organisation and project name. /// The user agent for the API client. /// The Accept header value expected by the GitHub API. /// The username with which to authenticate to the GitHub API. /// The password with which to authenticate to the GitHub API. - 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 @@ public GitHubClient(string baseUrl, string releaseUrlFormat, string userAgent, s /// Get the latest release for a GitHub repository. /// The repository key (like Pathoschild/SMAPI). - /// Returns the latest release if found, else null. - public async Task GetLatestReleaseAsync(string repo) + /// Whether to return a prerelease version if it's latest. + /// Returns the release if found, else null. + public async Task 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(); + return results.FirstOrDefault(); + } + return await this.Client - .GetAsync(string.Format(this.ReleaseUrlFormat, repo)) + .GetAsync(string.Format(this.StableReleaseUrlFormat, repo)) .As(); } catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound) @@ -66,5 +77,18 @@ public void Dispose() { this.Client?.Dispose(); } + + + /********* + ** Private methods + *********/ + /// Assert that a repository key is formatted correctly. + /// The repository key (like Pathoschild/SMAPI). + /// The repository key is invalid. + 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 b944088d5..827374fb6 100644 --- a/src/SMAPI.Web/Framework/Clients/GitHub/GitRelease.cs +++ b/src/SMAPI.Web/Framework/Clients/GitHub/GitRelease.cs @@ -19,6 +19,10 @@ internal class GitRelease /// The Markdown description for the release. public string Body { get; set; } + /// Whether this is a prerelease version. + [JsonProperty("prerelease")] + public bool IsPrerelease { get; set; } + /// The attached files. 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 6e8eadff2..9519c26f2 100644 --- a/src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs +++ b/src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs @@ -11,7 +11,8 @@ internal interface IGitHubClient : IDisposable *********/ /// Get the latest release for a GitHub repository. /// The repository key (like Pathoschild/SMAPI). - /// Returns the latest release if found, else null. - Task GetLatestReleaseAsync(string repo); + /// Whether to return a prerelease version if it's latest. + /// Returns the release if found, else null. + Task 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 612194143..de6c024a9 100644 --- a/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs +++ b/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs @@ -29,8 +29,11 @@ internal class ApiClientsConfig /// The base URL for the GitHub API. public string GitHubBaseUrl { get; set; } - /// The URL for a GitHub API latest-release query excluding the , where {0} is the organisation and project name. - public string GitHubReleaseUrlFormat { get; set; } + /// The URL for a GitHub API query for the latest stable release, excluding the , where {0} is the organisation and project name. + public string GitHubStableReleaseUrlFormat { get; set; } + + /// The URL for a GitHub API query for the latest release (including prerelease), excluding the , where {0} is the organisation and project name. + public string GitHubAnyReleaseUrlFormat { get; set; } /// The Accept header value expected by the GitHub API. public string GitHubAcceptHeader { get; set; } diff --git a/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs index 266055a67..3e5a42727 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs @@ -43,7 +43,7 @@ public override async Task GetModInfoAsync(string id) 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 7bad61271..59eb8cd1d 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs @@ -38,10 +38,21 @@ public override async Task GetModInfoAsync(string id) // 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 e1dc0fcc6..6411ad4ca 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs @@ -43,7 +43,7 @@ public override async Task GetModInfoAsync(string id) 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/Startup.cs b/src/SMAPI.Web/Startup.cs index d7d4d0740..47102e5c8 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -74,7 +74,8 @@ public void ConfigureServices(IServiceCollection services) services.AddSingleton(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/appsettings.json b/src/SMAPI.Web/appsettings.json index 3cf72ddbb..bfe827fa1 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 From 7015e4ee8777c4a1c3934c1f3e9c3ceefcc1295d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 15 Mar 2018 20:58:43 -0400 Subject: [PATCH 03/29] show prerelease SMAPI updates when updating from an older prerelease of the same version (#457) --- src/SMAPI/Program.cs | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 4bd40710c..e4b279f7f 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -541,8 +541,10 @@ private void CheckForUpdatesAsync(IModMetadata[] mods) 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)) + else if (this.IsValidUpdate(Constants.ApiVersion, new SemanticVersion(response.Version))) this.Monitor.Log($"You can update SMAPI to {response.Version}: {response.Url}", 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}: {response.Url}", LogLevel.Alert); else this.Monitor.Log(" SMAPI okay.", LogLevel.Trace); } @@ -656,6 +658,27 @@ orderby mod.DisplayName }).Start(); } + /// Get whether a given version should be offered to the user as an update. + /// The current semantic version. + /// The target semantic version. + 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; + } + /// Create a directory path if it doesn't exist. /// The directory path. private void VerifyPath(string path) From 90cdbdf7b29ddeee80b213957a02e0e5c691282e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 15 Mar 2018 21:23:36 -0400 Subject: [PATCH 04/29] link SMAPI update checks to smapi.io instead of GitHub (#457) --- src/SMAPI/Constants.cs | 4 ++-- src/SMAPI/Program.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index d91fa5fb4..7a497a534 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -81,8 +81,8 @@ public static class Constants /**** ** Internal ****/ - /// The GitHub repository to check for updates. - internal const string GitHubRepository = "Pathoschild/SMAPI"; + /// The URL of the SMAPI home page. + internal const string HomePageUrl = "https://smapi.io"; /// The file path for the SMAPI configuration file. internal static string ApiConfigPath => Path.Combine(Constants.ExecutionPath, $"{typeof(Program).Assembly.GetName().Name}.config.json"); diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index e4b279f7f..8c1ea2386 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -542,9 +542,9 @@ private void CheckForUpdatesAsync(IModMetadata[] mods) this.Monitor.Log($"Error: {response.Error}"); } else if (this.IsValidUpdate(Constants.ApiVersion, new SemanticVersion(response.Version))) - this.Monitor.Log($"You can update SMAPI to {response.Version}: {response.Url}", LogLevel.Alert); + 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}: {response.Url}", LogLevel.Alert); + this.Monitor.Log($"You can update SMAPI to {response.PreviewVersion}: {Constants.HomePageUrl}", LogLevel.Alert); else this.Monitor.Log(" SMAPI okay.", LogLevel.Trace); } From ff6df97ae8ca12f45aaba1776472f1dc10234b91 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 15 Mar 2018 21:26:03 -0400 Subject: [PATCH 05/29] fix error handling in update check API (#457) --- src/SMAPI.Web/Controllers/ModsApiController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index abae7db7d..c99c87fbb 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -115,9 +115,9 @@ public async Task> PostAsync([FromBody] ModSea 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 From 594d176d39691ff46b2c99fdfaa4299a4ea43616 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 15 Mar 2018 23:36:16 -0400 Subject: [PATCH 06/29] prepare home page for upcoming beta (#457) --- src/SMAPI.Web/Controllers/IndexController.cs | 53 ++++++++++++++++--- src/SMAPI.Web/ViewModels/IndexModel.cs | 28 ++++------ src/SMAPI.Web/ViewModels/IndexVersionModel.cs | 41 ++++++++++++++ src/SMAPI.Web/Views/Index/Index.cshtml | 35 +++++++++--- 4 files changed, 124 insertions(+), 33 deletions(-) create mode 100644 src/SMAPI.Web/ViewModels/IndexVersionModel.cs diff --git a/src/SMAPI.Web/Controllers/IndexController.cs b/src/SMAPI.Web/Controllers/IndexController.cs index 5d45118f4..0464e50a8 100644 --- a/src/SMAPI.Web/Controllers/IndexController.cs +++ b/src/SMAPI.Web/Controllers/IndexController.cs @@ -3,6 +3,7 @@ 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 @@ internal class IndexController : Controller private readonly IGitHubClient GitHub; /// The cache time for release info. - private readonly TimeSpan CacheTime = TimeSpan.FromMinutes(5); + private readonly TimeSpan CacheTime = TimeSpan.FromSeconds(1); + + /// The GitHub repository name to check for update. + private readonly string RepositoryName = "Pathoschild/SMAPI"; /********* @@ -42,17 +46,24 @@ public IndexController(IMemoryCache cache, IGitHubClient github) [HttpGet] public async Task 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 @@ private string GetDevDownloadUrl(GitRelease release) // fallback just in case return "https://github.com/pathoschild/SMAPI/releases"; } + + /// Get the latest beta download for a SMAPI release. + /// The SMAPI release. + 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/ViewModels/IndexModel.cs b/src/SMAPI.Web/ViewModels/IndexModel.cs index 6d3da91e6..4268c878a 100644 --- a/src/SMAPI.Web/ViewModels/IndexModel.cs +++ b/src/SMAPI.Web/ViewModels/IndexModel.cs @@ -6,17 +6,11 @@ public class IndexModel /********* ** Accessors *********/ - /// The latest SMAPI version. - public string LatestVersion { get; set; } + /// The latest stable SMAPI version. + public IndexVersionModel StableVersion { get; set; } - /// The Markdown description for the release. - public string Description { get; set; } - - /// The main download URL. - public string DownloadUrl { get; set; } - - /// The for-developers download URL. - public string DevDownloadUrl { get; set; } + /// The latest prerelease SMAPI version (if newer than ). + public IndexVersionModel BetaVersion { get; set; } /********* @@ -26,16 +20,12 @@ public class IndexModel public IndexModel() { } /// Construct an instance. - /// The latest SMAPI version. - /// The Markdown description for the release. - /// The main download URL. - /// The for-developers download URL. - internal IndexModel(string latestVersion, string description, string downloadUrl, string devDownloadUrl) + /// The latest stable SMAPI version. + /// The latest prerelease SMAPI version (if newer than ). + 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 000000000..4f63b979b --- /dev/null +++ b/src/SMAPI.Web/ViewModels/IndexVersionModel.cs @@ -0,0 +1,41 @@ +namespace StardewModdingAPI.Web.ViewModels +{ + /// The fields for a SMAPI version. + public class IndexVersionModel + { + /********* + ** Accessors + *********/ + /// The release version. + public string Version { get; set; } + + /// The Markdown description for the release. + public string Description { get; set; } + + /// The main download URL. + public string DownloadUrl { get; set; } + + /// The for-developers download URL (not applicable for prerelease versions). + public string DevDownloadUrl { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public IndexVersionModel() { } + + /// Construct an instance. + /// The release number. + /// The Markdown description for the release. + /// The main download URL. + /// The for-developers download URL (not applicable for prerelease versions). + 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 ad58898e1..4efb9f8a7 100644 --- a/src/SMAPI.Web/Views/Index/Index.cshtml +++ b/src/SMAPI.Web/Views/Index/Index.cshtml @@ -13,7 +13,11 @@

- Download SMAPI @Model.LatestVersion
+ Download SMAPI @Model.StableVersion.Version
+ @if (Model.BetaVersion != null) + { + Download SMAPI @Model.BetaVersion.Version
for Stardew Valley 1.3 beta

+ } Install guide
FAQs
@@ -25,12 +29,29 @@
  • Get help on Discord or in the forums
  • -

    What's new in SMAPI @Model.LatestVersion?

    -
    - @Html.Raw(Markdig.Markdown.ToHtml(Model.Description)) -
    +@if (Model.BetaVersion == null) +{ +

    What's new in SMAPI @Model.StableVersion.Version?

    +
    + @Html.Raw(Markdig.Markdown.ToHtml(Model.StableVersion.Description)) +
    +

    See the release notes and mod compatibility list for more info.

    +} +else +{ +

    What's new in...

    +

    SMAPI @Model.StableVersion.Version?

    +
    + @Html.Raw(Markdig.Markdown.ToHtml(Model.StableVersion.Description)) +
    +

    See the release notes and mod compatibility list for more info.

    -

    See the release notes and mod compatibility list for more info.

    +

    SMAPI @Model.BetaVersion.Version?

    +
    + @Html.Raw(Markdig.Markdown.ToHtml(Model.BetaVersion.Description)) +
    +

    See the release notes and mod compatibility list for more info.

    +}

    Donate to support SMAPI ♥

    @@ -62,7 +83,7 @@

    For mod creators

    From b5866c2c06bb0d3a993091e507f4eabb4aeaf8e7 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 15 Mar 2018 23:41:19 -0400 Subject: [PATCH 07/29] update release notes (#457) --- docs/release-notes.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 01048b4c0..393090f2c 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,11 +2,17 @@ ## 2.5.3 From ada351b163d928b5c01787e3ac3ad25ee6fe1ce4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 16 Mar 2018 20:28:16 -0400 Subject: [PATCH 08/29] reduce cache time for failed update checks to 5 minutes (#454) --- docs/release-notes.md | 1 + src/SMAPI.Web/Controllers/ModsApiController.cs | 12 ++++++++---- .../Framework/ConfigModels/ModUpdateCheckConfig.cs | 7 +++++-- src/SMAPI.Web/appsettings.json | 3 ++- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 393090f2c..9d6541337 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -21,6 +21,7 @@ * Fixed rare crash with some combinations of manifest fields and internal mod data. * Fixed update checks failing for Nexus Mods due to a change in their API. * Fixed update checks failing for some older mods with non-standard versions. + * Fixed failed update checks being cached for an hour (now cached 5 minutes). * Fixed error when a content pack needs a mod that couldn't be loaded. * Fixed Linux ["magic number is wrong" errors](https://github.com/mono/mono/issues/6752) by changing default terminal order. * Updated compatibility list and added update checks for more mods. diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index c99c87fbb..24517263b 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -29,8 +29,11 @@ internal class ModsApiController : Controller /// The cache in which to store mod metadata. private readonly IMemoryCache Cache; - /// The number of minutes update checks should be cached before refetching them. - private readonly int CacheMinutes; + /// The number of minutes successful update checks should be cached before refetching them. + private readonly int SuccessCacheMinutes; + + /// The number of minutes failed update checks should be cached before refetching them. + private readonly int ErrorCacheMinutes; /// A regex which matches SMAPI-style semantic version. private readonly string VersionRegex; @@ -50,7 +53,8 @@ public ModsApiController(IMemoryCache cache, IOptions conf 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[] @@ -121,7 +125,7 @@ public async Task> PostAsync([FromBody] ModSea } // 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/ConfigModels/ModUpdateCheckConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs index 58c3a100c..fc3b7dc25 100644 --- a/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs +++ b/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs @@ -6,8 +6,11 @@ internal class ModUpdateCheckConfig /********* ** Accessors *********/ - /// The number of minutes update checks should be cached before refetching them. - public int CacheMinutes { get; set; } + /// The number of minutes successful update checks should be cached before refetching them. + public int SuccessCacheMinutes { get; set; } + + /// The number of minutes failed update checks should be cached before refetching them. + public int ErrorCacheMinutes { get; set; } /// A regex which matches SMAPI-style semantic version. /// Derived from SMAPI's SemanticVersion implementation. diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json index bfe827fa1..03ca31ed8 100644 --- a/src/SMAPI.Web/appsettings.json +++ b/src/SMAPI.Web/appsettings.json @@ -40,7 +40,8 @@ }, "ModUpdateCheck": { - "CacheMinutes": 60, + "SuccessCacheMinutes": 60, + "ErrorCacheMinutes": 5, "SemanticVersionRegex": "^(?>(?0|[1-9]\\d*))\\.(?>(?0|[1-9]\\d*))(?>(?:\\.(?0|[1-9]\\d*))?)(?:-(?(?>[a-z0-9]+[\\-\\.]?)+))?$", "ChucklefishKey": "Chucklefish", From ae061165442ecb93e3a5a81bb918fd4c29e85e3a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 20 Mar 2018 00:49:14 -0400 Subject: [PATCH 09/29] fix minimum Stardew Valley 1.2 version mistakenly raised in 2.5.3 --- src/SMAPI/Constants.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 7a497a534..1279f8e1a 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -49,7 +49,7 @@ public static class Constants #if STARDEW_VALLEY_1_3 new GameVersion("1.3.0.4"); #else - new SemanticVersion("1.2.33"); + new SemanticVersion("1.2.30"); #endif /// The maximum supported version of Stardew Valley. From 5be3e5af5a08f9e72a969191dca07597d9c7c1f7 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 20 Mar 2018 19:45:45 -0400 Subject: [PATCH 10/29] rename class to better match usage (#459) --- src/SMAPI/Framework/ContentCore.cs | 4 ++-- .../Metadata/{CoreAssets.cs => CoreAssetPropagator.cs} | 7 ++++--- src/SMAPI/StardewModdingAPI.csproj | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) rename src/SMAPI/Metadata/{CoreAssets.cs => CoreAssetPropagator.cs} (97%) diff --git a/src/SMAPI/Framework/ContentCore.cs b/src/SMAPI/Framework/ContentCore.cs index 85b8db8fd..d5848d7b3 100644 --- a/src/SMAPI/Framework/ContentCore.cs +++ b/src/SMAPI/Framework/ContentCore.cs @@ -51,7 +51,7 @@ internal class ContentCore : IDisposable private readonly IDictionary LanguageCodes; /// Provides metadata for core game assets. - private readonly CoreAssets CoreAssets; + private readonly CoreAssetPropagator CoreAssets; /// The assets currently being intercepted by instances. This is used to prevent infinite loops when a loader loads a new asset. private readonly ContextHash AssetsBeingLoaded = new ContextHash(); @@ -103,7 +103,7 @@ public ContentCore(IServiceProvider serviceProvider, string rootDirectory, Cultu 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); } diff --git a/src/SMAPI/Metadata/CoreAssets.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs similarity index 97% rename from src/SMAPI/Metadata/CoreAssets.cs rename to src/SMAPI/Metadata/CoreAssetPropagator.cs index 87629682d..d6a731cdf 100644 --- a/src/SMAPI/Metadata/CoreAssets.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -14,8 +14,9 @@ namespace StardewModdingAPI.Metadata { - /// Provides metadata about core assets in the game. - internal class CoreAssets + /// Handles updating the game when a mod changes core assets. + /// This implementation only handles the core assets used by the game itself, and doesn't update any custom references to the changed textures. + internal class CoreAssetPropagator { /********* ** Properties @@ -33,7 +34,7 @@ internal class CoreAssets /// Initialise the core asset data. /// Normalises an asset key to match the cache key. /// Simplifies access to private code. - public CoreAssets(Func getNormalisedPath, Reflector reflection) + public CoreAssetPropagator(Func getNormalisedPath, Reflector reflection) { this.GetNormalisedPath = getNormalisedPath; this.SingletonSetters = diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index bffb96e29..82a5602de 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -137,7 +137,7 @@ - + From de5ee6f928339198d3c3ab0a91e9343863782c59 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 20 Mar 2018 21:22:19 -0400 Subject: [PATCH 11/29] rewrite core asset logic for extensibility (#459) --- src/SMAPI/Framework/ContentCore.cs | 2 +- src/SMAPI/Metadata/CoreAssetPropagator.cs | 379 ++++++++++++++-------- 2 files changed, 249 insertions(+), 132 deletions(-) diff --git a/src/SMAPI/Framework/ContentCore.cs b/src/SMAPI/Framework/ContentCore.cs index d5848d7b3..3c7e7b5a2 100644 --- a/src/SMAPI/Framework/ContentCore.cs +++ b/src/SMAPI/Framework/ContentCore.cs @@ -368,7 +368,7 @@ public bool InvalidateCache(Func predicate, bool dispose = f 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 index d6a731cdf..850217271 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -14,18 +14,17 @@ namespace StardewModdingAPI.Metadata { - /// Handles updating the game when a mod changes core assets. - /// This implementation only handles the core assets used by the game itself, and doesn't update any custom references to the changed textures. + /// Propagates changes to core assets to the game state. internal class CoreAssetPropagator { /********* ** Properties *********/ /// Normalises an asset key to match the cache key. - protected readonly Func GetNormalisedPath; + private readonly Func GetNormalisedPath; - /// Setters which update static or singleton texture fields indexed by normalised asset key. - private readonly IDictionary> SingletonSetters; + /// Simplifies access to private game code. + private readonly Reflector Reflection; /********* @@ -37,154 +36,272 @@ internal class CoreAssetPropagator public CoreAssetPropagator(Func getNormalisedPath, Reflector reflection) { this.GetNormalisedPath = getNormalisedPath; - this.SingletonSetters = - new Dictionary> - { - // from CraftingRecipe.InitShared - ["Data\\CraftingRecipes"] = (content, key) => CraftingRecipe.craftingRecipes = content.Load>(key), - ["Data\\CookingRecipes"] = (content, key) => CraftingRecipe.cookingRecipes = content.Load>(key), - - // from Game1.loadContent - ["LooseSprites\\daybg"] = (content, key) => Game1.daybg = content.Load(key), - ["LooseSprites\\nightbg"] = (content, key) => Game1.nightbg = content.Load(key), - ["Maps\\MenuTiles"] = (content, key) => Game1.menuTexture = content.Load(key), - ["LooseSprites\\Lighting\\lantern"] = (content, key) => Game1.lantern = content.Load(key), - ["LooseSprites\\Lighting\\windowLight"] = (content, key) => Game1.windowLight = content.Load(key), - ["LooseSprites\\Lighting\\sconceLight"] = (content, key) => Game1.sconceLight = content.Load(key), - ["LooseSprites\\Lighting\\greenLight"] = (content, key) => Game1.cauldronLight = content.Load(key), - ["LooseSprites\\Lighting\\indoorWindowLight"] = (content, key) => Game1.indoorWindowLight = content.Load(key), - ["LooseSprites\\shadow"] = (content, key) => Game1.shadowTexture = content.Load(key), - ["LooseSprites\\Cursors"] = (content, key) => Game1.mouseCursors = content.Load(key), - ["LooseSprites\\ControllerMaps"] = (content, key) => Game1.controllerMaps = content.Load(key), - ["TileSheets\\animations"] = (content, key) => Game1.animations = content.Load(key), - ["Data\\Achievements"] = (content, key) => Game1.achievements = content.Load>(key), - ["Data\\NPCGiftTastes"] = (content, key) => Game1.NPCGiftTastes = content.Load>(key), - ["Fonts\\SpriteFont1"] = (content, key) => Game1.dialogueFont = content.Load(key), - ["Fonts\\SmallFont"] = (content, key) => Game1.smallFont = content.Load(key), - ["Fonts\\tinyFont"] = (content, key) => Game1.tinyFont = content.Load(key), - ["Fonts\\tinyFontBorder"] = (content, key) => Game1.tinyFontBorder = content.Load(key), - ["Maps\\springobjects"] = (content, key) => Game1.objectSpriteSheet = content.Load(key), - ["TileSheets\\crops"] = (content, key) => Game1.cropSpriteSheet = content.Load(key), - ["TileSheets\\emotes"] = (content, key) => Game1.emoteSpriteSheet = content.Load(key), - ["TileSheets\\debris"] = (content, key) => Game1.debrisSpriteSheet = content.Load(key), - ["TileSheets\\Craftables"] = (content, key) => Game1.bigCraftableSpriteSheet = content.Load(key), - ["TileSheets\\rain"] = (content, key) => Game1.rainTexture = content.Load(key), - ["TileSheets\\BuffsIcons"] = (content, key) => Game1.buffsIcons = content.Load(key), - ["Data\\ObjectInformation"] = (content, key) => Game1.objectInformation = content.Load>(key), - ["Data\\BigCraftablesInformation"] = (content, key) => Game1.bigCraftablesInformation = content.Load>(key), - ["Characters\\Farmer\\hairstyles"] = (content, key) => FarmerRenderer.hairStylesTexture = content.Load(key), - ["Characters\\Farmer\\shirts"] = (content, key) => FarmerRenderer.shirtsTexture = content.Load(key), - ["Characters\\Farmer\\hats"] = (content, key) => FarmerRenderer.hatsTexture = content.Load(key), - ["Characters\\Farmer\\accessories"] = (content, key) => FarmerRenderer.accessoriesTexture = content.Load(key), - ["TileSheets\\furniture"] = (content, key) => Furniture.furnitureTexture = content.Load(key), - ["LooseSprites\\font_bold"] = (content, key) => SpriteText.spriteTexture = content.Load(key), - ["LooseSprites\\font_colored"] = (content, key) => SpriteText.coloredTexture = content.Load(key), - ["TileSheets\\weapons"] = (content, key) => Tool.weaponsTexture = content.Load(key), - ["TileSheets\\Projectiles"] = (content, key) => Projectile.projectileSheet = content.Load(key), - - // from Game1.ResetToolSpriteSheet - ["TileSheets\\tools"] = (content, key) => Game1.ResetToolSpriteSheet(), + this.Reflection = reflection; + } -#if STARDEW_VALLEY_1_3 - // from Bush - ["TileSheets\\bushes"] = (content, key) => reflection.GetField>(typeof(Bush), "texture").SetValue(new Lazy(() => content.Load(key))), + /// Reload one of the game's core assets (if applicable). + /// The content manager through which to reload the asset. + /// The asset key to reload. + /// Returns whether an asset was reloaded. + public bool Propagate(LocalizedContentManager content, string key) + { + return this.PropagateImpl(content, key) != null; + } - // from Farm - ["Buildings\\houses"] = (content, key) => reflection.GetField(typeof(Farm), nameof(Farm.houseTextures)).SetValue(content.Load(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) => + /********* + ** Private methods + *********/ + /// Reload one of the game's core assets (if applicable). + /// The content manager through which to reload the asset. + /// The asset key to reload. + /// Returns any non-null value to indicate an asset was loaded.. + private object PropagateImpl(LocalizedContentManager content, string key) + { + Reflector reflection = this.Reflection; + switch (key.ToLower().Replace("/", "\\")) // normalised key so we can compare statically + { + /**** + ** Buildings + ****/ + case "buildings\\houses": // Farm +#if STARDEW_VALLEY_1_3 + reflection.GetField(typeof(Farm), nameof(Farm.houseTextures)).SetValue(content.Load(key)); + return true; +#else { - if (Game1.player != null && !Game1.player.isMale) - Game1.player.FarmerRenderer = new FarmerRenderer(key); - }, + Farm farm = Game1.getFarm(); + if (farm == null) + return null; + return farm.houseTextures = content.Load(key); + } +#endif + + /**** + ** Content\Characters\Farmer + ****/ + case "characters\\farmer\\accessories": // Game1.loadContent + return FarmerRenderer.accessoriesTexture = content.Load(key); + + case "characters\\farmer\\farmer_base": // Farmer + if (Game1.player == null || !Game1.player.isMale) + return null; +#if STARDEW_VALLEY_1_3 + return Game1.player.FarmerRenderer = new FarmerRenderer(key); #else - // from Bush - ["TileSheets\\bushes"] = (content, key) => Bush.texture = content.Load(key), + return Game1.player.FarmerRenderer = new FarmerRenderer(content.Load(key)); +#endif - // from Critter - ["TileSheets\\critters"] = (content, key) => Critter.critterTexture = content.Load(key), + case "characters\\farmer\\farmer_girl_base": // Farmer + if (Game1.player == null || Game1.player.isMale) + return null; +#if STARDEW_VALLEY_1_3 + return Game1.player.FarmerRenderer = new FarmerRenderer(key); +#else + return Game1.player.FarmerRenderer = new FarmerRenderer(content.Load(key)); +#endif - // from Farm - ["Buildings\\houses"] = (content, key) => - { - Farm farm = Game1.getFarm(); - if (farm != null) - farm.houseTextures = content.Load(key); - }, + case "characters\\farmer\\hairstyles": // Game1.loadContent + return FarmerRenderer.hairStylesTexture = content.Load(key); + + case "characters\\farmer\\hats": // Game1.loadContent + return FarmerRenderer.hatsTexture = content.Load(key); + + case "characters\\farmer\\shirts": // Game1.loadContent + return FarmerRenderer.shirtsTexture = content.Load(key); + + /**** + ** Content\Data + ****/ + case "data\\achievements": // Game1.loadContent + return Game1.achievements = content.Load>(key); + + case "data\\bigcraftablesinformation": // Game1.loadContent + return Game1.bigCraftablesInformation = content.Load>(key); + + case "data\\cookingrecipes": // CraftingRecipe.InitShared + return CraftingRecipe.cookingRecipes = content.Load>(key); + + case "data\\craftingrecipes": // CraftingRecipe.InitShared + return CraftingRecipe.craftingRecipes = content.Load>(key); + + case "data\\npcgifttastes": // Game1.loadContent + return Game1.NPCGiftTastes = content.Load>(key); + + case "data\\objectinformation": // Game1.loadContent + return Game1.objectInformation = content.Load>(key); + + /**** + ** Content\Fonts + ****/ + case "fonts\\spritefont1": // Game1.loadContent + return Game1.dialogueFont = content.Load(key); + + case "fonts\\smallfont": // Game1.loadContent + return Game1.smallFont = content.Load(key); + + case "fonts\\tinyfont": // Game1.loadContent + return Game1.tinyFont = content.Load(key); + + case "fonts\\tinyfontborder": // Game1.loadContent + return Game1.tinyFontBorder = content.Load(key); - // from Farmer - ["Characters\\Farmer\\farmer_base"] = (content, key) => + /**** + ** Content\Lighting + ****/ + case "loosesprites\\lighting\\greenlight": // Game1.loadContent + return Game1.cauldronLight = content.Load(key); + + case "loosesprites\\lighting\\indoorwindowlight": // Game1.loadContent + return Game1.indoorWindowLight = content.Load(key); + + case "loosesprites\\lighting\\lantern": // Game1.loadContent + return Game1.lantern = content.Load(key); + + case "loosesprites\\lighting\\sconcelight": // Game1.loadContent + return Game1.sconceLight = content.Load(key); + + case "loosesprites\\lighting\\windowlight": // Game1.loadContent + return Game1.windowLight = content.Load(key); + + /**** + ** Content\LooseSprites + ****/ + case "loosesprites\\controllermaps": // Game1.loadContent + return Game1.controllerMaps = content.Load(key); + + case "loosesprites\\cursors": // Game1.loadContent + return Game1.mouseCursors = content.Load(key); + + case "loosesprites\\daybg": // Game1.loadContent + return Game1.daybg = content.Load(key); + + case "loosesprites\\font_bold": // Game1.loadContent + return SpriteText.spriteTexture = content.Load(key); + + case "loosesprites\\font_colored": // Game1.loadContent + return SpriteText.coloredTexture = content.Load(key); + + case "loosesprites\\nightbg": // Game1.loadContent + return Game1.nightbg = content.Load(key); + + case "loosesprites\\shadow": // Game1.loadContent + return Game1.shadowTexture = content.Load(key); + + /**** + ** Content\Critters + ****/ + case "tilesheets\\critters": // Criter.InitShared + return Critter.critterTexture = content.Load(key); + + case "tilesheets\\crops": // Game1.loadContent + return Game1.cropSpriteSheet = content.Load(key); + + case "tilesheets\\debris": // Game1.loadContent + return Game1.debrisSpriteSheet = content.Load(key); + + case "tilesheets\\emotes": // Game1.loadContent + return Game1.emoteSpriteSheet = content.Load(key); + + case "tilesheets\\furniture": // Game1.loadContent + return Furniture.furnitureTexture = content.Load(key); + + case "tilesheets\\projectiles": // Game1.loadContent + return Projectile.projectileSheet = content.Load(key); + + case "tilesheets\\rain": // Game1.loadContent + return Game1.rainTexture = content.Load(key); + + case "tilesheets\\tools": // Game1.ResetToolSpriteSheet + Game1.ResetToolSpriteSheet(); + return true; + + case "tilesheets\\weapons": // Game1.loadContent + return Tool.weaponsTexture = content.Load(key); + + /**** + ** Content\Maps + ****/ + case "maps\\menutiles": // Game1.loadContent + return Game1.menuTexture = content.Load(key); + + case "maps\\springobjects": // Game1.loadContent + return Game1.objectSpriteSheet = content.Load(key); + + case "maps\\walls_and_floors": // Wallpaper + return Wallpaper.wallpaperTexture = content.Load(key); + + /**** + ** Content\Minigames + ****/ + case "minigames\\clouds": // TitleMenu + if (Game1.activeClickableMenu is TitleMenu) { - if (Game1.player != null && Game1.player.isMale) - Game1.player.FarmerRenderer = new FarmerRenderer(content.Load(key)); - }, - ["Characters\\Farmer\\farmer_girl_base"] = (content, key) => + reflection.GetField(Game1.activeClickableMenu, "cloudsTexture").SetValue(content.Load(key)); + return true; + } + + return null; + + case "minigames\\titlebuttons": // TitleMenu + if (Game1.activeClickableMenu is TitleMenu titleMenu) { - if (Game1.player != null && !Game1.player.isMale) - Game1.player.FarmerRenderer = new FarmerRenderer(content.Load(key)); - }, + Texture2D texture = content.Load(key); + reflection.GetField(titleMenu, "titleButtonsTexture").SetValue(texture); + foreach (TemporaryAnimatedSprite bird in reflection.GetField>(titleMenu, "birds").GetValue()) +#if STARDEW_VALLEY_1_3 + bird.texture = texture; +#else + bird.Texture = texture; #endif + return true; + } - // from Flooring - ["TerrainFeatures\\Flooring"] = (content, key) => Flooring.floorsTexture = content.Load(key), + return null; - // from FruitTree - ["TileSheets\\fruitTrees"] = (content, key) => FruitTree.texture = content.Load(key), + /**** + ** Content\TileSheets + ****/ + case "tilesheets\\animations": // Game1.loadContent + return Game1.animations = content.Load(key); - // from HoeDirt - ["TerrainFeatures\\hoeDirt"] = (content, key) => HoeDirt.lightTexture = content.Load(key), - ["TerrainFeatures\\hoeDirtDark"] = (content, key) => HoeDirt.darkTexture = content.Load(key), - ["TerrainFeatures\\hoeDirtSnow"] = (content, key) => HoeDirt.snowTexture = content.Load(key), + case "tilesheets\\buffsicons": // Game1.loadContent + return Game1.buffsIcons = content.Load(key); - // from TitleMenu - ["Minigames\\Clouds"] = (content, key) => - { - if (Game1.activeClickableMenu is TitleMenu) - reflection.GetField(Game1.activeClickableMenu, "cloudsTexture").SetValue(content.Load(key)); - }, - ["Minigames\\TitleButtons"] = (content, key) => - { - if (Game1.activeClickableMenu is TitleMenu titleMenu) - { - reflection.GetField(titleMenu, "titleButtonsTexture").SetValue(content.Load(key)); - foreach (TemporaryAnimatedSprite bird in reflection.GetField>(titleMenu, "birds").GetValue()) + case "tilesheets\\bushes": // new Bush() #if STARDEW_VALLEY_1_3 - bird.texture = content.Load(key); + reflection.GetField>(typeof(Bush), "texture").SetValue(new Lazy(() => content.Load(key))); + return true; #else - bird.Texture = content.Load(key); + return Bush.texture = content.Load(key); #endif - } - }, - // from Wallpaper - ["Maps\\walls_and_floors"] = (content, key) => Wallpaper.wallpaperTexture = content.Load(key) - } - .ToDictionary(p => getNormalisedPath(p.Key), p => p.Value); - } + case "tilesheets\\craftables": // Game1.loadContent + return Game1.bigCraftableSpriteSheet = content.Load(key); - /// Reload one of the game's core assets (if applicable). - /// The content manager through which to reload the asset. - /// The asset key to reload. - /// Returns whether an asset was reloaded. - public bool ReloadForKey(LocalizedContentManager content, string key) - { - // static assets - if (this.SingletonSetters.TryGetValue(key, out Action reload)) - { - reload(content, key); - return true; + case "tilesheets\\fruittrees": // FruitTree + return FruitTree.texture = content.Load(key); + + /**** + ** Content\TerrainFeatures + ****/ + case "terrainfeatures\\flooring": // Flooring + return Flooring.floorsTexture = content.Load(key); + + case "terrainfeatures\\hoedirt": // from HoeDirt + return HoeDirt.lightTexture = content.Load(key); + + case "Terrainfeatures\\hoedirtdark": // from HoeDirt + return HoeDirt.darkTexture = content.Load(key); + + case "Terrainfeatures\\hoedirtsnow": // from HoeDirt + return HoeDirt.snowTexture = content.Load(key); } // building textures - if (key.StartsWith(this.GetNormalisedPath("Buildings\\"))) + if (key.StartsWith(this.GetNormalisedPath("Buildings\\"), StringComparison.InvariantCultureIgnoreCase)) { - Building[] buildings = this.GetAllBuildings().Where(p => key == this.GetNormalisedPath($"Buildings\\{p.buildingType}")).ToArray(); + Building[] buildings = this.GetAllBuildings().Where(p => key.Equals(this.GetNormalisedPath($"Buildings\\{p.buildingType?.ToLower()}"), StringComparison.InvariantCultureIgnoreCase)).ToArray(); if (buildings.Any()) { #if STARDEW_VALLEY_1_3 @@ -198,10 +315,10 @@ public bool ReloadForKey(LocalizedContentManager content, string key) return true; } - return false; + return null; } - return false; + return null; } From e48f2301423f5177ec875308348fd4a83a071c3b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 21 Mar 2018 00:19:12 -0400 Subject: [PATCH 12/29] add unit test mode to mod build config package --- docs/mod-build-config.md | 13 +++++++++++++ src/SMAPI.ModBuildConfig/build/smapi.targets | 17 +++++++++++++++++ src/SMAPI.ModBuildConfig/package.nuspec | 3 ++- 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/docs/mod-build-config.md b/docs/mod-build-config.md index ca750c86c..2616d8a59 100644 --- a/docs/mod-build-config.md +++ b/docs/mod-build-config.md @@ -120,6 +120,19 @@ or you have multiple installs, you can specify the path yourself. There's two wa The configuration will check your custom path first, then fall back to the default paths (so it'll still compile on a different computer). +### Unit test projects +**(upcoming in 2.0.3)** + +You can use the package in unit test projects too. Its optional unit test mode... + +1. disables deploying the project as a mod; +2. disables creating a release zip; +2. and copies the referenced DLLs into the build output for unit test frameworks. + +To enable it, add this above the first `` in your `.csproj`: +```xml +True +``` ## Troubleshoot ### "Failed to find the game install path" diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets index 7e8bbfc34..e27fc2c7f 100644 --- a/src/SMAPI.ModBuildConfig/build/smapi.targets +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -19,9 +19,14 @@ $(MSBuildProjectName) + True $(TargetDir) True True + + + False + False @@ -57,32 +62,40 @@ false + true false + true false + true false + true $(GamePath)\Netcode.dll False + true $(GamePath)\Stardew Valley.exe false + true $(GamePath)\StardewModdingAPI.exe false + true $(GamePath)\xTile.dll false False + true @@ -100,18 +113,22 @@ $(GamePath)\MonoGame.Framework.dll false False + true $(GamePath)\StardewValley.exe false + true $(GamePath)\StardewModdingAPI.exe false + true $(GamePath)\xTile.dll false + true diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec index 8393ab61a..6af8fefe9 100644 --- a/src/SMAPI.ModBuildConfig/package.nuspec +++ b/src/SMAPI.ModBuildConfig/package.nuspec @@ -2,7 +2,7 @@ Pathoschild.Stardew.ModBuildConfig - 2.0.3-alpha20180307 + 2.0.3-alpha20180321 Build package for SMAPI mods Pathoschild Pathoschild @@ -29,6 +29,7 @@ 2.0.3: - Added support for Stardew Valley 1.3. + - Added support for unit test projects. From 5b765849d87a702bc79affb82ecb9d565f57b30c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 22 Mar 2018 20:54:05 -0400 Subject: [PATCH 13/29] fix unit test check in build config package --- src/SMAPI.ModBuildConfig/build/smapi.targets | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets index e27fc2c7f..d2e37101e 100644 --- a/src/SMAPI.ModBuildConfig/build/smapi.targets +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -19,14 +19,14 @@ $(MSBuildProjectName) - True + False $(TargetDir) True True - False - False + False + False From 91561eedc7c8247a6179e0158eb9f9affdf65012 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 23 Mar 2018 01:21:50 -0400 Subject: [PATCH 14/29] fix log parser errors when log text contains {{tokens}} --- docs/release-notes.md | 4 +++ src/SMAPI.Web/Views/LogParser/Index.cshtml | 36 +++++++++++----------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 9d6541337..059105c35 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -15,6 +15,10 @@ * Added support for beta releases on the home page. --> +## 2.5.4 +* For the [log parser][]: + * Fixed error when log text contains certain tokens. + ## 2.5.3 * For players: * Simplified and improved skipped-mod messages. diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index d2d8004eb..9c21c8c08 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -54,23 +54,23 @@ Game info: SMAPI version: - @Model.ParsedLog.ApiVersion + @Model.ParsedLog.ApiVersion Game version: - @Model.ParsedLog.GameVersion + @Model.ParsedLog.GameVersion Platform: - @Model.ParsedLog.OperatingSystem + @Model.ParsedLog.OperatingSystem Mods path: - @Model.ParsedLog.ModPath + @Model.ParsedLog.ModPath Log started: - @Model.ParsedLog.Timestamp.UtcDateTime.ToString("yyyy-MM-dd HH:mm") UTC ({{localTimeStarted}} your time) + @Model.ParsedLog.Timestamp.UtcDateTime.ToString("yyyy-MM-dd HH:mm") UTC ({{localTimeStarted}} your time)
    @@ -85,7 +85,7 @@ { - + @mod.Name @if (contentPacks != null && contentPacks.TryGetValue(mod.Name, out LogModInfo[] contentPackList)) { @@ -97,19 +97,19 @@
    } - @mod.Version - @mod.Author + @mod.Version + @mod.Author @if (mod.Errors == 0) { - no errors + no errors } else if (mod.Errors == 1) { - @mod.Errors error + @mod.Errors error } else { - @mod.Errors errors + @mod.Errors errors } } @@ -130,16 +130,16 @@ string levelStr = message.Level.ToString().ToLower(); - @message.Time - @message.Level.ToString().ToUpper() - @message.Mod - @message.Text + @message.Time + @message.Level.ToString().ToUpper() + @message.Mod + @message.Text if (message.Repeated > 0) { - repeats [@message.Repeated] times. + repeats [@message.Repeated] times. } } @@ -151,11 +151,11 @@ else if (Model.ParsedLog?.IsValid == false)

    Parsed log

    We couldn't parse that file, but you can still share the link.

    -

    Error details: @Model.ParsedLog.Error

    +

    Error details: @Model.ParsedLog.Error

    Raw log

    -
    @Model.ParsedLog.RawText
    +
    @Model.ParsedLog.RawText
    }
    From 51368b8afb0c2064a70ed09f41570ca8fac5fdfa Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 23 Mar 2018 20:18:23 -0400 Subject: [PATCH 15/29] update tree textures when changeed through the content API (#459) --- docs/release-notes.md | 3 + src/SMAPI/Metadata/CoreAssetPropagator.cs | 120 ++++++++++++++++------ 2 files changed, 91 insertions(+), 32 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 059105c35..05a37ea87 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -16,6 +16,9 @@ --> ## 2.5.4 +* For modders: + * Added automatic texture update for trees when changed through the content API. + * For the [log parser][]: * Fixed error when log text contains certain tokens. diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 850217271..21aaeb6c4 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Framework.Reflection; @@ -45,7 +46,10 @@ public CoreAssetPropagator(Func getNormalisedPath, Reflector ref /// Returns whether an asset was reloaded. public bool Propagate(LocalizedContentManager content, string key) { - return this.PropagateImpl(content, key) != null; + object result = this.PropagateImpl(content, key); + if (result is bool b) + return b; + return result != null; } @@ -55,7 +59,7 @@ public bool Propagate(LocalizedContentManager content, string key) /// Reload one of the game's core assets (if applicable). /// The content manager through which to reload the asset. /// The asset key to reload. - /// Returns any non-null value to indicate an asset was loaded.. + /// Returns any non-null value to indicate an asset was loaded. private object PropagateImpl(LocalizedContentManager content, string key) { Reflector reflection = this.Reflection; @@ -72,7 +76,7 @@ private object PropagateImpl(LocalizedContentManager content, string key) { Farm farm = Game1.getFarm(); if (farm == null) - return null; + return false; return farm.houseTextures = content.Load(key); } #endif @@ -85,7 +89,7 @@ private object PropagateImpl(LocalizedContentManager content, string key) case "characters\\farmer\\farmer_base": // Farmer if (Game1.player == null || !Game1.player.isMale) - return null; + return false; #if STARDEW_VALLEY_1_3 return Game1.player.FarmerRenderer = new FarmerRenderer(key); #else @@ -94,7 +98,7 @@ private object PropagateImpl(LocalizedContentManager content, string key) case "characters\\farmer\\farmer_girl_base": // Farmer if (Game1.player == null || Game1.player.isMale) - return null; + return false; #if STARDEW_VALLEY_1_3 return Game1.player.FarmerRenderer = new FarmerRenderer(key); #else @@ -240,8 +244,7 @@ private object PropagateImpl(LocalizedContentManager content, string key) reflection.GetField(Game1.activeClickableMenu, "cloudsTexture").SetValue(content.Load(key)); return true; } - - return null; + return false; case "minigames\\titlebuttons": // TitleMenu if (Game1.activeClickableMenu is TitleMenu titleMenu) @@ -256,8 +259,7 @@ private object PropagateImpl(LocalizedContentManager content, string key) #endif return true; } - - return null; + return false; /**** ** Content\TileSheets @@ -291,48 +293,102 @@ private object PropagateImpl(LocalizedContentManager content, string key) case "terrainfeatures\\hoedirt": // from HoeDirt return HoeDirt.lightTexture = content.Load(key); - case "Terrainfeatures\\hoedirtdark": // from HoeDirt + case "terrainfeatures\\hoedirtdark": // from HoeDirt return HoeDirt.darkTexture = content.Load(key); - case "Terrainfeatures\\hoedirtsnow": // from HoeDirt + case "terrainfeatures\\hoedirtsnow": // from HoeDirt return HoeDirt.snowTexture = content.Load(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); } // building textures if (key.StartsWith(this.GetNormalisedPath("Buildings\\"), StringComparison.InvariantCultureIgnoreCase)) { - Building[] buildings = this.GetAllBuildings().Where(p => key.Equals(this.GetNormalisedPath($"Buildings\\{p.buildingType?.ToLower()}"), StringComparison.InvariantCultureIgnoreCase)).ToArray(); - if (buildings.Any()) - { -#if STARDEW_VALLEY_1_3 - foreach (Building building in buildings) - building.texture = new Lazy(() => content.Load(key)); -#else - Texture2D texture = content.Load(key); - foreach (Building building in buildings) - building.texture = texture; -#endif - - return true; - } - return null; + string type = Path.GetFileName(key); + return this.ReloadBuildings(content, key, type); } - return null; + return false; } /********* ** Private methods *********/ - /// Get all player-constructed buildings in the world. - private IEnumerable GetAllBuildings() + /// Reload building textures. + /// The content manager through which to reload the asset. + /// The asset key to reload. + /// The type to reload. + /// Returns whether any textures were reloaded. + private bool ReloadBuildings(LocalizedContentManager content, string key, string type) + { + Building[] buildings = Game1.locations + .OfType() + .SelectMany(p => p.buildings) + .Where(p => p.buildingType == type) + .ToArray(); + + if (buildings.Any()) + { + Lazy texture = new Lazy(() => content.Load(key)); + foreach (Building building in buildings) +#if STARDEW_VALLEY_1_3 + building.texture = texture; +#else + building.texture = texture.Value; +#endif + return true; + } + return false; + } + + /// Reload tree textures. + /// The content manager through which to reload the asset. + /// The asset key to reload. + /// The type to reload. + /// Returns whether any textures were reloaded. + private bool ReloadTreeTextures(LocalizedContentManager content, string key, int type) { - foreach (BuildableGameLocation location in Game1.locations.OfType()) + Tree[] trees = Game1.locations + .SelectMany(p => p.terrainFeatures.Values.OfType()) + .Where(tree => tree.treeType == type) + .ToArray(); + + if (trees.Any()) { - foreach (Building building in location.buildings) - yield return building; + Lazy texture = new Lazy(() => content.Load(key)); + foreach (Tree tree in trees) +#if STARDEW_VALLEY_1_3 + this.Reflection.GetField>(tree, "texture").SetValue(texture); +#else + this.Reflection.GetField(tree, "texture").SetValue(texture.Value); +#endif + return true; } + + return false; } } } From fad47ff74f6d03652951230eb1c394b896578c48 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 23 Mar 2018 22:30:49 -0400 Subject: [PATCH 16/29] fix image overlay bugs on Linux/Mac (#461) --- docs/release-notes.md | 5 +++-- src/SMAPI/Framework/Content/AssetDataForImage.cs | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 05a37ea87..c40e41be6 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -16,8 +16,9 @@ --> ## 2.5.4 -* For modders: - * Added automatic texture update for trees when changed through the content API. +* For players: + * Fixed tree textures not updated when changed through the content API. + * Fixed display bugs on Linux/Mac when mods overlay images through the content API. * For the [log parser][]: * Fixed error when log text contains certain tokens. diff --git a/src/SMAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs index c665484fb..fc653bcfc 100644 --- a/src/SMAPI/Framework/Content/AssetDataForImage.cs +++ b/src/SMAPI/Framework/Content/AssetDataForImage.cs @@ -58,7 +58,7 @@ public void PatchImage(Texture2D source, Rectangle? sourceArea = null, Rectangle for (int i = 0; i < sourceData.Length; i++) { Color pixel = sourceData[i]; - if (pixel.A != 0) // not transparent + if (pixel.A > 2) // not transparent (note: on Linux/Mac, fully transparent pixels may have an alpha up to 2 for some reason) newData[i] = pixel; } sourceData = newData; From 5126d56b3992acd3193aaae35f178bb5e9da1cae Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 23 Mar 2018 22:41:15 -0400 Subject: [PATCH 17/29] fix error when a mod removes an asset editor/loader (#460) --- docs/release-notes.md | 3 ++- src/SMAPI/Program.cs | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index c40e41be6..33e66a147 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -18,7 +18,8 @@ ## 2.5.4 * For players: * Fixed tree textures not updated when changed through the content API. - * Fixed display bugs on Linux/Mac when mods overlay images through the content API. + * Fixed visual bug on Linux/Mac when mods overlay images through the content API. + * Fixed error when a mod removes an asset editor/loader. * For the [log parser][]: * Fixed error when log text contains certain tokens. diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 8c1ea2386..1b8cb2ba9 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -953,7 +953,7 @@ IContentPack CreateTransitionalContentPack(string packDirPath, IManifest packMan { 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().ToArray(), new IAssetLoader[0]); @@ -961,7 +961,7 @@ IContentPack CreateTransitionalContentPack(string packDirPath, IManifest packMan }; 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().ToArray()); From 34346d8b0911ec949832c037bfd83d4ab706d37a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 24 Mar 2018 19:06:13 -0400 Subject: [PATCH 18/29] tweak transparency threshold (#461) --- src/SMAPI/Framework/Content/AssetDataForImage.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SMAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs index fc653bcfc..1eef2afb3 100644 --- a/src/SMAPI/Framework/Content/AssetDataForImage.cs +++ b/src/SMAPI/Framework/Content/AssetDataForImage.cs @@ -58,7 +58,7 @@ public void PatchImage(Texture2D source, Rectangle? sourceArea = null, Rectangle for (int i = 0; i < sourceData.Length; i++) { Color pixel = sourceData[i]; - if (pixel.A > 2) // not transparent (note: on Linux/Mac, fully transparent pixels may have an alpha up to 2 for some reason) + 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; From 20b778390051569aa34a9ac42f03cd9b9a051df7 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 24 Mar 2018 20:26:33 -0400 Subject: [PATCH 19/29] update NPC textures when changed through the content API (#459) --- src/SMAPI/Metadata/CoreAssetPropagator.cs | 122 ++++++++++++++++++++-- 1 file changed, 115 insertions(+), 7 deletions(-) diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 21aaeb6c4..4a1d60971 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -323,12 +323,18 @@ private object PropagateImpl(LocalizedContentManager content, string key) return this.ReloadTreeTextures(content, key, Tree.pineTree); } - // building textures + // dynamic textures if (key.StartsWith(this.GetNormalisedPath("Buildings\\"), StringComparison.InvariantCultureIgnoreCase)) - { - string type = Path.GetFileName(key); - return this.ReloadBuildings(content, key, type); - } + return this.ReloadBuildings(content, key); + + if (key.StartsWith(this.GetNormalisedPath("Characters\\")) && this.CountSegments(key) == 2) // ignore Characters/Dialogue/*, etc + return this.ReloadNpcSprites(content, key, monster: false); + + if (key.StartsWith(this.GetNormalisedPath("Characters\\Monsters\\"))) + return this.ReloadNpcSprites(content, key, monster: true); + + if (key.StartsWith(this.GetNormalisedPath("Portraits\\"))) + return this.ReloadNpcPortraits(content, key); return false; } @@ -340,16 +346,18 @@ private object PropagateImpl(LocalizedContentManager content, string key) /// Reload building textures. /// The content manager through which to reload the asset. /// The asset key to reload. - /// The type to reload. /// Returns whether any textures were reloaded. - private bool ReloadBuildings(LocalizedContentManager content, string key, string type) + private bool ReloadBuildings(LocalizedContentManager content, string key) { + // get buildings + string type = Path.GetFileName(key); Building[] buildings = Game1.locations .OfType() .SelectMany(p => p.buildings) .Where(p => p.buildingType == type) .ToArray(); + // reload buildings if (buildings.Any()) { Lazy texture = new Lazy(() => content.Load(key)); @@ -364,6 +372,61 @@ private bool ReloadBuildings(LocalizedContentManager content, string key, string return false; } + /// Reload the sprites for matching NPCs. + /// The content manager through which to reload the asset. + /// The asset key to reload. + /// Whether to match monsters (true) or non-monsters (false). + /// Returns whether any textures were reloaded. + private bool ReloadNpcSprites(LocalizedContentManager content, string key, bool monster) + { + // get NPCs + string name = this.GetNpcNameFromFileName(Path.GetFileName(key)); + NPC[] characters = + ( + from location in this.GetLocations() + from npc in location.characters + where npc.name == name && npc.IsMonster == monster + select npc + ) + .Distinct() + .ToArray(); + if (!characters.Any()) + return false; + + // update portrait + Texture2D texture = content.Load(key); + foreach (NPC character in characters) + character.Sprite.Texture = texture; + return true; + } + + /// Reload the portraits for matching NPCs. + /// The content manager through which to reload the asset. + /// The asset key to reload. + /// Returns whether any textures were reloaded. + private bool ReloadNpcPortraits(LocalizedContentManager content, string key) + { + // get NPCs + string name = this.GetNpcNameFromFileName(Path.GetFileName(key)); + NPC[] villagers = + ( + from location in this.GetLocations() + from npc in location.characters + where npc.name == name && npc.isVillager() + select npc + ) + .Distinct() + .ToArray(); + if (!villagers.Any()) + return false; + + // update portrait + Texture2D texture = content.Load(key); + foreach (NPC villager in villagers) + villager.Portrait = texture; + return true; + } + /// Reload tree textures. /// The content manager through which to reload the asset. /// The asset key to reload. @@ -390,5 +453,50 @@ private bool ReloadTreeTextures(LocalizedContentManager content, string key, int return false; } + + /// Get an NPC name from the name of their file under Content/Characters. + /// The file name. + /// Derived from . + 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; + } + } + + /// Get all locations in the game. + private IEnumerable 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; + } + } + } + } + + /// Count the number of segments in a path (e.g. 'a/b' is 2). + /// The path to check. + private int CountSegments(string path) + { + if (path == null) + return 0; + return path.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar).Length; + } } } From d0b96ed3c068f8b94a53a156a7c5e668f53aa2fc Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 24 Mar 2018 20:33:59 -0400 Subject: [PATCH 20/29] update release notes (#459) --- docs/release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 33e66a147..e0e209a1d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -17,7 +17,7 @@ ## 2.5.4 * For players: - * Fixed tree textures not updated when changed through the content API. + * Fixed NPC and tree textures not updated when changed through the content API. * Fixed visual bug on Linux/Mac when mods overlay images through the content API. * Fixed error when a mod removes an asset editor/loader. From 5a0e49827be92d19dfdda7bb15ca15fa8f269ecb Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 25 Mar 2018 00:52:37 -0400 Subject: [PATCH 21/29] update fence textures when changed through the content API (#459) --- docs/release-notes.md | 3 +- src/SMAPI/Metadata/CoreAssetPropagator.cs | 44 +++++++++++++++++++++-- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index e0e209a1d..2a134d13d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -17,9 +17,10 @@ ## 2.5.4 * For players: - * Fixed NPC and tree textures not updated when changed through the content API. + * Fixed fence, NPC, and tree textures not updated when a mod changes them through the content API. * Fixed visual bug on Linux/Mac when mods overlay images through the content API. * Fixed error when a mod removes an asset editor/loader. + * Fixed minimum game version incorrectly changed from 1.2.30 to 1.2.33 in SMAPI 2.5.3. * For the [log parser][]: * Fixed error when log text contains certain tokens. diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 4a1d60971..1702ee265 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -333,6 +333,9 @@ private object PropagateImpl(LocalizedContentManager content, string key) if (key.StartsWith(this.GetNormalisedPath("Characters\\Monsters\\"))) return this.ReloadNpcSprites(content, key, monster: true); + if (key.StartsWith(this.GetNormalisedPath("LooseSprites\\Fence"))) + return this.ReloadFenceTextures(content, key); + if (key.StartsWith(this.GetNormalisedPath("Portraits\\"))) return this.ReloadNpcPortraits(content, key); @@ -372,6 +375,34 @@ private bool ReloadBuildings(LocalizedContentManager content, string key) return false; } + /// Reload the sprites for a fence type. + /// The content manager through which to reload the asset. + /// The asset key to reload. + /// Returns whether any textures were reloaded. + 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() + where fenceType == 1 + ? fence.isGate + : fence.whichType == fenceType + select fence + ) + .ToArray(); + + // update fence textures + foreach (Fence fence in fences) + fence.reloadSprite(); + return true; + } + /// Reload the sprites for matching NPCs. /// The content manager through which to reload the asset. /// The asset key to reload. @@ -490,13 +521,20 @@ private IEnumerable GetLocations() } } + /// Get the segments in a path (e.g. 'a/b' is 'a' and 'b'). + /// The path to check. + private string[] GetSegments(string path) + { + if (path == null) + return new string[0]; + return path.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + } + /// Count the number of segments in a path (e.g. 'a/b' is 2). /// The path to check. private int CountSegments(string path) { - if (path == null) - return 0; - return path.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar).Length; + return this.GetSegments(path).Length; } } } From b1cc6c1d9995db2eccead2a2c99f8f64ddb1da81 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 25 Mar 2018 11:41:56 -0400 Subject: [PATCH 22/29] update new asset update logic for Stardew Valley 1.3 (#453) --- src/SMAPI/Metadata/CoreAssetPropagator.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 1702ee265..277da5250 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -195,8 +195,10 @@ private object PropagateImpl(LocalizedContentManager content, string key) /**** ** Content\Critters ****/ +#if !STARDEW_VALLEY_1_3 case "tilesheets\\critters": // Criter.InitShared return Critter.critterTexture = content.Load(key); +#endif case "tilesheets\\crops": // Game1.loadContent return Game1.cropSpriteSheet = content.Load(key); @@ -427,7 +429,11 @@ select npc // update portrait Texture2D texture = content.Load(key); foreach (NPC character in characters) +#if STARDEW_VALLEY_1_3 + this.Reflection.GetField(character.Sprite, "spriteTexture").SetValue(texture); +#else character.Sprite.Texture = texture; +#endif return true; } From 5681c0f98170d5830e7f2ab58c77b92216aa9a60 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 25 Mar 2018 12:01:19 -0400 Subject: [PATCH 23/29] update mod build config package --- docs/mod-build-config.md | 10 ++++++++++ src/SMAPI.ModBuildConfig/package.nuspec | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/mod-build-config.md b/docs/mod-build-config.md index 2616d8a59..0d72e4d9c 100644 --- a/docs/mod-build-config.md +++ b/docs/mod-build-config.md @@ -140,6 +140,16 @@ That error means the package couldn't find your game. You can specify the game p _[Game path](#game-path)_ above. ## Release notes +### 2.0.3 alpha +* Added support for Stardew Valley 1.3. +* Added support for unit test projects. + +### 2.0.2 +* Fixed compatibility issue on Linux. + +### 2.0.1 +* Fixed mod deploy failing to create subfolders if they don't already exist. + ### 2.0 * Added: mods are now copied into the `Mods` folder automatically (configurable). * Added: release zips are now created automatically in your build output folder (configurable). diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec index 6af8fefe9..d24e15bec 100644 --- a/src/SMAPI.ModBuildConfig/package.nuspec +++ b/src/SMAPI.ModBuildConfig/package.nuspec @@ -2,7 +2,7 @@ Pathoschild.Stardew.ModBuildConfig - 2.0.3-alpha20180321 + 2.0.3-alpha20180325 Build package for SMAPI mods Pathoschild Pathoschild From 4d668eb7022819391a6c1834e1351c9b4fc52104 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 25 Mar 2018 12:17:58 -0400 Subject: [PATCH 24/29] update API packages --- src/SMAPI.Web/StardewModdingAPI.Web.csproj | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/SMAPI.Web/StardewModdingAPI.Web.csproj b/src/SMAPI.Web/StardewModdingAPI.Web.csproj index 19198503c..e2eee8a8b 100644 --- a/src/SMAPI.Web/StardewModdingAPI.Web.csproj +++ b/src/SMAPI.Web/StardewModdingAPI.Web.csproj @@ -10,13 +10,13 @@ - - - - - - - + + + + + + + From 04e299aeaa4f9291c15f49d8093831cae93e8329 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 25 Mar 2018 12:32:16 -0400 Subject: [PATCH 25/29] update Json.NET package --- .../StardewModdingAPI.Mods.ConsoleCommands.csproj | 4 ++-- src/SMAPI.Mods.ConsoleCommands/packages.config | 2 +- src/SMAPI/StardewModdingAPI.csproj | 3 ++- src/SMAPI/packages.config | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj b/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj index a5b89a336..d1f72c6c6 100644 --- a/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj +++ b/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj @@ -37,8 +37,8 @@ - ..\packages\Newtonsoft.Json.11.0.1-beta3\lib\net45\Newtonsoft.Json.dll - False + ..\packages\Newtonsoft.Json.11.0.2\lib\net45\Newtonsoft.Json.dll + True diff --git a/src/SMAPI.Mods.ConsoleCommands/packages.config b/src/SMAPI.Mods.ConsoleCommands/packages.config index a0f76c34a..c8b3ae63a 100644 --- a/src/SMAPI.Mods.ConsoleCommands/packages.config +++ b/src/SMAPI.Mods.ConsoleCommands/packages.config @@ -1,4 +1,4 @@  - + \ No newline at end of file diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 82a5602de..edddbd2aa 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -66,7 +66,8 @@ True - ..\packages\Newtonsoft.Json.11.0.1-beta3\lib\net45\Newtonsoft.Json.dll + ..\packages\Newtonsoft.Json.11.0.2\lib\net45\Newtonsoft.Json.dll + True diff --git a/src/SMAPI/packages.config b/src/SMAPI/packages.config index 1a0b78fa9..3e8769224 100644 --- a/src/SMAPI/packages.config +++ b/src/SMAPI/packages.config @@ -1,5 +1,5 @@  - + \ No newline at end of file From 0bcc1f6be95af4a309ff9bb31750f29b4b8338df Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 25 Mar 2018 13:28:38 -0400 Subject: [PATCH 26/29] standardise folder checks when reloading assets (#459) --- src/SMAPI/Metadata/CoreAssetPropagator.cs | 27 ++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 277da5250..38b205a57 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -326,19 +326,19 @@ private object PropagateImpl(LocalizedContentManager content, string key) } // dynamic textures - if (key.StartsWith(this.GetNormalisedPath("Buildings\\"), StringComparison.InvariantCultureIgnoreCase)) + if (this.IsInFolder(key, "Buildings")) return this.ReloadBuildings(content, key); - if (key.StartsWith(this.GetNormalisedPath("Characters\\")) && this.CountSegments(key) == 2) // ignore Characters/Dialogue/*, etc + if (this.IsInFolder(key, "Characters")) return this.ReloadNpcSprites(content, key, monster: false); - if (key.StartsWith(this.GetNormalisedPath("Characters\\Monsters\\"))) + if (this.IsInFolder(key, "Characters\\Monsters")) return this.ReloadNpcSprites(content, key, monster: true); - if (key.StartsWith(this.GetNormalisedPath("LooseSprites\\Fence"))) + if (key.StartsWith(this.GetNormalisedPath("LooseSprites\\Fence"), StringComparison.InvariantCultureIgnoreCase)) return this.ReloadFenceTextures(content, key); - if (key.StartsWith(this.GetNormalisedPath("Portraits\\"))) + if (this.IsInFolder(key, "Portraits")) return this.ReloadNpcPortraits(content, key); return false; @@ -348,6 +348,9 @@ private object PropagateImpl(LocalizedContentManager content, string key) /********* ** Private methods *********/ + /**** + ** Reload methods + ****/ /// Reload building textures. /// The content manager through which to reload the asset. /// The asset key to reload. @@ -491,6 +494,9 @@ private bool ReloadTreeTextures(LocalizedContentManager content, string key, int return false; } + /**** + ** Helpers + ****/ /// Get an NPC name from the name of their file under Content/Characters. /// The file name. /// Derived from . @@ -527,6 +533,17 @@ private IEnumerable GetLocations() } } + /// Get whether a normalised asset key is in the given folder. + /// The normalised asset key (like Animals/cat). + /// The key folder (like Animals); doesn't need to be normalised. + /// Whether to return true if the key is inside a subfolder of the . + 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); + } + /// Get the segments in a path (e.g. 'a/b' is 'a' and 'b'). /// The path to check. private string[] GetSegments(string path) From 60fc4a64886d92e70475af5bbfeb29b2e3ef26cd Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 25 Mar 2018 14:59:06 -0400 Subject: [PATCH 27/29] update animal textures when changed through the content API (#459) --- docs/release-notes.md | 4 +- src/SMAPI/Metadata/CoreAssetPropagator.cs | 125 ++++++++++++++++++---- 2 files changed, 104 insertions(+), 25 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 2a134d13d..c30d3a3b6 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -17,8 +17,8 @@ ## 2.5.4 * For players: - * Fixed fence, NPC, and tree textures not updated when a mod changes them through the content API. - * Fixed visual bug on Linux/Mac when mods overlay images through the content API. + * Fixed some textures not updated when a mod changes them (notably animals, fences, NPCs, and trees). + * Fixed visual bug on Linux/Mac when mods overlay textures. * Fixed error when a mod removes an asset editor/loader. * Fixed minimum game version incorrectly changed from 1.2.30 to 1.2.33 in SMAPI 2.5.3. diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 38b205a57..e54e02866 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -7,6 +7,7 @@ using StardewValley; using StardewValley.BellsAndWhistles; using StardewValley.Buildings; +using StardewValley.Characters; using StardewValley.Locations; using StardewValley.Menus; using StardewValley.Objects; @@ -65,6 +66,16 @@ 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(content, key); + case "animals\\dog": + return this.ReloadPetOrHorseSprites(content, key); + case "animals\\horse": + return this.ReloadPetOrHorseSprites(content, key); + /**** ** Buildings ****/ @@ -326,6 +337,9 @@ private object PropagateImpl(LocalizedContentManager content, string key) } // dynamic textures + if (this.IsInFolder(key, "Animals")) + return this.ReloadFarmAnimalSprites(content, key); + if (this.IsInFolder(key, "Buildings")) return this.ReloadBuildings(content, key); @@ -351,6 +365,57 @@ private object PropagateImpl(LocalizedContentManager content, string key) /**** ** Reload methods ****/ + /// Reload the sprites for matching pets or horses. + /// The animal type. + /// The content manager through which to reload the asset. + /// The asset key to reload. + /// Returns whether any textures were reloaded. + private bool ReloadPetOrHorseSprites(LocalizedContentManager content, string key) + where TAnimal : NPC + { + // find matches + TAnimal[] animals = this.GetCharacters().OfType().ToArray(); + if (!animals.Any()) + return false; + + // update sprites + Texture2D texture = content.Load(key); + foreach (TAnimal animal in animals) + this.SetSpriteTexture(animal.sprite, texture); + return true; + } + + /// Reload the sprites for matching farm animals. + /// The content manager through which to reload the asset. + /// The asset key to reload. + /// Returns whether any textures were reloaded. + /// Derived from . + private bool ReloadFarmAnimalSprites(LocalizedContentManager content, string key) + { + // find matches + FarmAnimal[] animals = this.GetFarmAnimals().ToArray(); + if (!animals.Any()) + return false; + + // update sprites + Lazy texture = new Lazy(() => content.Load(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; + } + /// Reload building textures. /// The content manager through which to reload the asset. /// The asset key to reload. @@ -417,26 +482,14 @@ private bool ReloadNpcSprites(LocalizedContentManager content, string key, bool { // get NPCs string name = this.GetNpcNameFromFileName(Path.GetFileName(key)); - NPC[] characters = - ( - from location in this.GetLocations() - from npc in location.characters - where npc.name == name && npc.IsMonster == monster - select npc - ) - .Distinct() - .ToArray(); + NPC[] characters = this.GetCharacters().Where(npc => npc.name == name && npc.IsMonster == monster).ToArray(); if (!characters.Any()) return false; // update portrait Texture2D texture = content.Load(key); foreach (NPC character in characters) -#if STARDEW_VALLEY_1_3 - this.Reflection.GetField(character.Sprite, "spriteTexture").SetValue(texture); -#else - character.Sprite.Texture = texture; -#endif + this.SetSpriteTexture(character.Sprite, texture); return true; } @@ -448,15 +501,7 @@ private bool ReloadNpcPortraits(LocalizedContentManager content, string key) { // get NPCs string name = this.GetNpcNameFromFileName(Path.GetFileName(key)); - NPC[] villagers = - ( - from location in this.GetLocations() - from npc in location.characters - where npc.name == name && npc.isVillager() - select npc - ) - .Distinct() - .ToArray(); + NPC[] villagers = this.GetCharacters().Where(npc => npc.name == name && npc.isVillager()).ToArray(); if (!villagers.Any()) return false; @@ -497,6 +542,18 @@ private bool ReloadTreeTextures(LocalizedContentManager content, string key, int /**** ** Helpers ****/ + /// Reload the texture for an animated sprite. + /// The animated sprite to update. + /// The texture to set. + private void SetSpriteTexture(AnimatedSprite sprite, Texture2D texture) + { +#if STARDEW_VALLEY_1_3 + this.Reflection.GetField(sprite, "spriteTexture").SetValue(texture); +#else + sprite.Texture = texture; +#endif + } + /// Get an NPC name from the name of their file under Content/Characters. /// The file name. /// Derived from . @@ -515,6 +572,28 @@ private string GetNpcNameFromFileName(string name) } } + /// Get all NPCs in the game (excluding farm animals). + private IEnumerable GetCharacters() + { + return this.GetLocations().SelectMany(p => p.characters); + } + + /// Get all farm animals in the game. + private IEnumerable 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; + } + } + /// Get all locations in the game. private IEnumerable GetLocations() { From 56288e1d0ec072d35040b7954fc7c0f8b086663e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 26 Mar 2018 09:22:45 -0400 Subject: [PATCH 28/29] fix log parser timestamp not rendered --- src/SMAPI.Web/Views/LogParser/Index.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index 9c21c8c08..7213e2863 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -70,7 +70,7 @@ Log started: - @Model.ParsedLog.Timestamp.UtcDateTime.ToString("yyyy-MM-dd HH:mm") UTC ({{localTimeStarted}} your time) + @Model.ParsedLog.Timestamp.UtcDateTime.ToString("yyyy-MM-dd HH:mm") UTC ({{localTimeStarted}} your time)
    From 4d68ef3514de7deb357a0042d1af7ccf241ab5ff Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 26 Mar 2018 09:34:45 -0400 Subject: [PATCH 29/29] update for 2.5.4 release --- build/GlobalAssemblyInfo.cs | 4 ++-- docs/release-notes.md | 12 +++++++++--- src/SMAPI.Mods.ConsoleCommands/manifest.json | 2 +- src/SMAPI/Constants.cs | 2 +- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/build/GlobalAssemblyInfo.cs b/build/GlobalAssemblyInfo.cs index e9236498a..d2cf8fe76 100644 --- a/build/GlobalAssemblyInfo.cs +++ b/build/GlobalAssemblyInfo.cs @@ -1,5 +1,5 @@ using System.Reflection; [assembly: AssemblyProduct("SMAPI")] -[assembly: AssemblyVersion("2.5.3")] -[assembly: AssemblyFileVersion("2.5.3")] +[assembly: AssemblyVersion("2.5.4")] +[assembly: AssemblyFileVersion("2.5.4")] diff --git a/docs/release-notes.md b/docs/release-notes.md index c30d3a3b6..b33008008 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -17,14 +17,20 @@ ## 2.5.4 * For players: - * Fixed some textures not updated when a mod changes them (notably animals, fences, NPCs, and trees). + * Fixed some textures not updated when a mod changes them. * Fixed visual bug on Linux/Mac when mods overlay textures. - * Fixed error when a mod removes an asset editor/loader. - * Fixed minimum game version incorrectly changed from 1.2.30 to 1.2.33 in SMAPI 2.5.3. + * Fixed error when mods remove an asset editor/loader. + * Fixed minimum game version incorrectly increased in SMAPI 2.5.3. * For the [log parser][]: * Fixed error when log text contains certain tokens. +* For modders: + * Updated to Json.NET 11.0.2. + +* For SMAPI developers: + * Added support for beta update track to support upcoming Stardew Valley 1.3 beta. + ## 2.5.3 * For players: * Simplified and improved skipped-mod messages. diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index 785af01aa..a56cf66d8 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/Constants.cs b/src/SMAPI/Constants.cs index 1279f8e1a..6270186a7 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -41,7 +41,7 @@ public static class Constants #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 /// The minimum supported version of Stardew Valley.