From e00fb85ee7822bc7fed2d6bd5a2e4c207a799418 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 7 Jul 2019 00:29:22 -0400 Subject: migrate compatibility list's wiki data to MongoDB cache (#651) --- .../Framework/Caching/BaseCacheRepository.cs | 19 +++ .../Framework/Caching/Wiki/CachedWikiMetadata.cs | 43 +++++ .../Framework/Caching/Wiki/CachedWikiMod.cs | 188 +++++++++++++++++++++ .../Framework/Caching/Wiki/IWikiCacheRepository.cs | 35 ++++ .../Framework/Caching/Wiki/WikiCacheRepository.cs | 73 ++++++++ .../Framework/ConfigModels/MongoDbConfig.cs | 36 ++++ 6 files changed, 394 insertions(+) create mode 100644 src/SMAPI.Web/Framework/Caching/BaseCacheRepository.cs create mode 100644 src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMetadata.cs create mode 100644 src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs create mode 100644 src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs create mode 100644 src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheRepository.cs create mode 100644 src/SMAPI.Web/Framework/ConfigModels/MongoDbConfig.cs (limited to 'src/SMAPI.Web/Framework') diff --git a/src/SMAPI.Web/Framework/Caching/BaseCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/BaseCacheRepository.cs new file mode 100644 index 00000000..904455c5 --- /dev/null +++ b/src/SMAPI.Web/Framework/Caching/BaseCacheRepository.cs @@ -0,0 +1,19 @@ +using System; + +namespace StardewModdingAPI.Web.Framework.Caching +{ + /// The base logic for a cache repository. + internal abstract class BaseCacheRepository + { + /********* + ** Public methods + *********/ + /// Whether cached data is stale. + /// The date when the data was updated. + /// The age in minutes before data is considered stale. + public bool IsStale(DateTimeOffset lastUpdated, int cacheMinutes) + { + return lastUpdated < DateTimeOffset.UtcNow.AddMinutes(-cacheMinutes); + } + } +} diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMetadata.cs b/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMetadata.cs new file mode 100644 index 00000000..de1ea9db --- /dev/null +++ b/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMetadata.cs @@ -0,0 +1,43 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using MongoDB.Bson; + +namespace StardewModdingAPI.Web.Framework.Caching.Wiki +{ + /// The model for cached wiki metadata. + public class CachedWikiMetadata + { + /********* + ** Accessors + *********/ + /// The internal MongoDB ID. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Named per MongoDB conventions.")] + public ObjectId _id { get; set; } + + /// When the data was last updated. + public DateTimeOffset LastUpdated { get; set; } + + /// The current stable Stardew Valley version. + public string StableVersion { get; set; } + + /// The current beta Stardew Valley version. + public string BetaVersion { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public CachedWikiMetadata() { } + + /// Construct an instance. + /// The current stable Stardew Valley version. + /// The current beta Stardew Valley version. + public CachedWikiMetadata(string stableVersion, string betaVersion) + { + this.StableVersion = stableVersion; + this.BetaVersion = betaVersion; + this.LastUpdated = DateTimeOffset.UtcNow;; + } + } +} diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs b/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs new file mode 100644 index 00000000..f4331f8d --- /dev/null +++ b/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs @@ -0,0 +1,188 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using MongoDB.Bson; +using StardewModdingAPI.Toolkit; +using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; + +namespace StardewModdingAPI.Web.Framework.Caching.Wiki +{ + /// The model for cached wiki mods. + public class CachedWikiMod + { + /********* + ** Accessors + *********/ + /**** + ** Tracking + ****/ + /// The internal MongoDB ID. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Named per MongoDB conventions.")] + public ObjectId _id { get; set; } + + /// When the data was last updated. + public DateTimeOffset LastUpdated { get; set; } + + /**** + ** Mod info + ****/ + /// The mod's unique ID. If the mod has alternate/old IDs, they're listed in latest to newest order. + public string[] ID { get; set; } + + /// The mod's display name. If the mod has multiple names, the first one is the most canonical name. + public string[] Name { get; set; } + + /// The mod's author name. If the author has multiple names, the first one is the most canonical name. + public string[] Author { get; set; } + + /// The mod ID on Nexus. + public int? NexusID { get; set; } + + /// The mod ID in the Chucklefish mod repo. + public int? ChucklefishID { get; set; } + + /// The mod ID in the ModDrop mod repo. + public int? ModDropID { get; set; } + + /// The GitHub repository in the form 'owner/repo'. + public string GitHubRepo { get; set; } + + /// The URL to a non-GitHub source repo. + public string CustomSourceUrl { get; set; } + + /// The custom mod page URL (if applicable). + public string CustomUrl { get; set; } + + /// The name of the mod which loads this content pack, if applicable. + public string ContentPackFor { get; set; } + + /// The human-readable warnings for players about this mod. + public string[] Warnings { get; set; } + + /// The link anchor for the mod entry in the wiki compatibility list. + public string Anchor { get; set; } + + /**** + ** Stable compatibility + ****/ + /// The compatibility status. + public WikiCompatibilityStatus MainStatus { get; set; } + + /// The human-readable summary of the compatibility status or workaround, without HTML formatting. + public string MainSummary { get; set; } + + /// The game or SMAPI version which broke this mod (if applicable). + public string MainBrokeIn { get; set; } + + /// The version of the latest unofficial update, if applicable. + public string MainUnofficialVersion { get; set; } + + /// The URL to the latest unofficial update, if applicable. + public string MainUnofficialUrl { get; set; } + + /**** + ** Beta compatibility + ****/ + /// The compatibility status. + public WikiCompatibilityStatus? BetaStatus { get; set; } + + /// The human-readable summary of the compatibility status or workaround, without HTML formatting. + public string BetaSummary { get; set; } + + /// The game or SMAPI version which broke this mod (if applicable). + public string BetaBrokeIn { get; set; } + + /// The version of the latest unofficial update, if applicable. + public string BetaUnofficialVersion { get; set; } + + /// The URL to the latest unofficial update, if applicable. + public string BetaUnofficialUrl { get; set; } + + + /********* + ** Accessors + *********/ + /// Construct an instance. + public CachedWikiMod() { } + + /// Construct an instance. + /// The mod data. + public CachedWikiMod(WikiModEntry mod) + { + // tracking + this.LastUpdated = DateTimeOffset.UtcNow; + + // mod info + this.ID = mod.ID; + this.Name = mod.Name; + this.Author = mod.Author; + this.NexusID = mod.NexusID; + this.ChucklefishID = mod.ChucklefishID; + this.ModDropID = mod.ModDropID; + this.GitHubRepo = mod.GitHubRepo; + this.CustomSourceUrl = mod.CustomSourceUrl; + this.CustomUrl = mod.CustomUrl; + this.ContentPackFor = mod.ContentPackFor; + this.Warnings = mod.Warnings; + this.Anchor = mod.Anchor; + + // stable compatibility + this.MainStatus = mod.Compatibility.Status; + this.MainSummary = mod.Compatibility.Summary; + this.MainBrokeIn = mod.Compatibility.BrokeIn; + this.MainUnofficialVersion = mod.Compatibility.UnofficialVersion?.ToString(); + this.MainUnofficialUrl = mod.Compatibility.UnofficialUrl; + + // beta compatibility + this.BetaStatus = mod.BetaCompatibility?.Status; + this.BetaSummary = mod.BetaCompatibility?.Summary; + this.BetaBrokeIn = mod.BetaCompatibility?.BrokeIn; + this.BetaUnofficialVersion = mod.BetaCompatibility?.UnofficialVersion?.ToString(); + this.BetaUnofficialUrl = mod.BetaCompatibility?.UnofficialUrl; + } + + /// Reconstruct the original model. + public WikiModEntry GetModel() + { + var mod = new WikiModEntry + { + ID = this.ID, + Name = this.Name, + Author = this.Author, + NexusID = this.NexusID, + ChucklefishID = this.ChucklefishID, + ModDropID = this.ModDropID, + GitHubRepo = this.GitHubRepo, + CustomSourceUrl = this.CustomSourceUrl, + CustomUrl = this.CustomUrl, + ContentPackFor = this.ContentPackFor, + Warnings = this.Warnings, + Anchor = this.Anchor, + + // stable compatibility + Compatibility = new WikiCompatibilityInfo + { + Status = this.MainStatus, + Summary = this.MainSummary, + BrokeIn = this.MainBrokeIn, + UnofficialVersion = this.MainUnofficialVersion != null ? new SemanticVersion(this.MainUnofficialVersion) : null, + UnofficialUrl = this.MainUnofficialUrl + } + }; + + // beta compatibility + if (this.BetaStatus != null) + { + mod.BetaCompatibility = new WikiCompatibilityInfo + { + Status = this.BetaStatus.Value, + Summary = this.BetaSummary, + BrokeIn = this.BetaBrokeIn, + UnofficialVersion = this.BetaUnofficialVersion != null ? new SemanticVersion(this.BetaUnofficialVersion) : null, + UnofficialUrl = this.BetaUnofficialUrl + }; + } + + return mod; + } + } +} diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs new file mode 100644 index 00000000..d319db69 --- /dev/null +++ b/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; + +namespace StardewModdingAPI.Web.Framework.Caching.Wiki +{ + /// Encapsulates logic for accessing the mod data cache. + internal interface IWikiCacheRepository + { + /********* + ** Methods + *********/ + /// Get the cached wiki metadata. + /// The fetched metadata. + bool TryGetWikiMetadata(out CachedWikiMetadata metadata); + + /// Whether cached data is stale. + /// The date when the data was updated. + /// The age in minutes before data is considered stale. + bool IsStale(DateTimeOffset lastUpdated, int cacheMinutes); + + /// Get the cached wiki mods. + /// A filter to apply, if any. + IEnumerable GetWikiMods(Expression> filter = null); + + /// Save data fetched from the wiki compatibility list. + /// The current stable Stardew Valley version. + /// The current beta Stardew Valley version. + /// The mod data. + /// The stored metadata record. + /// The stored mod records. + void SaveWikiData(string stableVersion, string betaVersion, IEnumerable mods, out CachedWikiMetadata cachedMetadata, out CachedWikiMod[] cachedMods); + } +} diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheRepository.cs new file mode 100644 index 00000000..5b907462 --- /dev/null +++ b/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheRepository.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using MongoDB.Driver; +using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; + +namespace StardewModdingAPI.Web.Framework.Caching.Wiki +{ + /// Encapsulates logic for accessing the wiki data cache. + internal class WikiCacheRepository : BaseCacheRepository, IWikiCacheRepository + { + /********* + ** Fields + *********/ + /// The collection for wiki metadata. + private readonly IMongoCollection WikiMetadata; + + /// The collection for wiki mod data. + private readonly IMongoCollection WikiMods; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The authenticated MongoDB database. + public WikiCacheRepository(IMongoDatabase database) + { + // get collections + this.WikiMetadata = database.GetCollection("wiki-metadata"); + this.WikiMods = database.GetCollection("wiki-mods"); + + // add indexes if needed + this.WikiMods.Indexes.CreateOne(new CreateIndexModel(Builders.IndexKeys.Text(p => p.ID))); + } + + /// Get the cached wiki metadata. + /// The fetched metadata. + public bool TryGetWikiMetadata(out CachedWikiMetadata metadata) + { + metadata = this.WikiMetadata.Find("{}").FirstOrDefault(); + return metadata != null; + } + + /// Get the cached wiki mods. + /// A filter to apply, if any. + public IEnumerable GetWikiMods(Expression> filter = null) + { + return filter != null + ? this.WikiMods.Find(filter).ToList() + : this.WikiMods.Find("{}").ToList(); + } + + /// Save data fetched from the wiki compatibility list. + /// The current stable Stardew Valley version. + /// The current beta Stardew Valley version. + /// The mod data. + /// The stored metadata record. + /// The stored mod records. + public void SaveWikiData(string stableVersion, string betaVersion, IEnumerable mods, out CachedWikiMetadata cachedMetadata, out CachedWikiMod[] cachedMods) + { + cachedMetadata = new CachedWikiMetadata(stableVersion, betaVersion); + cachedMods = mods.Select(mod => new CachedWikiMod(mod)).ToArray(); + + this.WikiMods.DeleteMany("{}"); + this.WikiMods.InsertMany(cachedMods); + + this.WikiMetadata.DeleteMany("{}"); + this.WikiMetadata.InsertOne(cachedMetadata); + } + } +} diff --git a/src/SMAPI.Web/Framework/ConfigModels/MongoDbConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/MongoDbConfig.cs new file mode 100644 index 00000000..352eb960 --- /dev/null +++ b/src/SMAPI.Web/Framework/ConfigModels/MongoDbConfig.cs @@ -0,0 +1,36 @@ +using System; + +namespace StardewModdingAPI.Web.Framework.ConfigModels +{ + /// The config settings for mod compatibility list. + internal class MongoDbConfig + { + /********* + ** Accessors + *********/ + /// The MongoDB hostname. + public string Host { get; set; } + + /// The MongoDB username (if any). + public string Username { get; set; } + + /// The MongoDB password (if any). + public string Password { get; set; } + + + /********* + ** Public method + *********/ + /// Get the MongoDB connection string. + /// The initial database for which to authenticate. + public string GetConnectionString(string authDatabase) + { + bool isLocal = this.Host == "localhost"; + bool hasLogin = !string.IsNullOrWhiteSpace(this.Username) && !string.IsNullOrWhiteSpace(this.Password); + + return $"mongodb{(isLocal ? "" : "+srv")}://" + + (hasLogin ? $"{Uri.EscapeDataString(this.Username)}:{Uri.EscapeDataString(this.Password)}@" : "") + + $"{this.Host}/{authDatabase}retryWrites=true&w=majority"; + } + } +} -- cgit From 2b3f0e740bc09630e881c9967e5ad53d66384094 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 7 Jul 2019 00:39:52 -0400 Subject: make MongoDB database name configurable (#651) --- docs/technical/web.md | 1 + src/SMAPI.Web/Framework/ConfigModels/MongoDbConfig.cs | 8 +++++--- src/SMAPI.Web/Startup.cs | 4 ++-- src/SMAPI.Web/appsettings.Development.json | 3 ++- src/SMAPI.Web/appsettings.json | 3 ++- 5 files changed, 12 insertions(+), 7 deletions(-) (limited to 'src/SMAPI.Web/Framework') diff --git a/docs/technical/web.md b/docs/technical/web.md index db54a87a..f1cb755b 100644 --- a/docs/technical/web.md +++ b/docs/technical/web.md @@ -123,6 +123,7 @@ Initial setup: `MongoDB:Host` | The hostname for the MongoDB instance. `MongoDB:Username` | The login username for the MongoDB instance. `MongoDB:Password` | The login password for the MongoDB instance. + `MongoDB:Database` | The database name (e.g. `smapi` in production or `smapi-edge` in testing environments). To deploy updates: 1. Deploy the web project using [AWS Toolkit for Visual Studio](https://aws.amazon.com/visualstudio/). diff --git a/src/SMAPI.Web/Framework/ConfigModels/MongoDbConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/MongoDbConfig.cs index 352eb960..3c508300 100644 --- a/src/SMAPI.Web/Framework/ConfigModels/MongoDbConfig.cs +++ b/src/SMAPI.Web/Framework/ConfigModels/MongoDbConfig.cs @@ -17,20 +17,22 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels /// The MongoDB password (if any). public string Password { get; set; } + /// The database name. + public string Database { get; set; } + /********* ** Public method *********/ /// Get the MongoDB connection string. - /// The initial database for which to authenticate. - public string GetConnectionString(string authDatabase) + public string GetConnectionString() { bool isLocal = this.Host == "localhost"; bool hasLogin = !string.IsNullOrWhiteSpace(this.Username) && !string.IsNullOrWhiteSpace(this.Password); return $"mongodb{(isLocal ? "" : "+srv")}://" + (hasLogin ? $"{Uri.EscapeDataString(this.Username)}:{Uri.EscapeDataString(this.Password)}@" : "") - + $"{this.Host}/{authDatabase}retryWrites=true&w=majority"; + + $"{this.Host}/{this.Database}?retryWrites=true&w=majority"; } } } diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index 0a8d23a8..315f5f88 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -115,9 +115,9 @@ namespace StardewModdingAPI.Web // init MongoDB { MongoDbConfig mongoConfig = this.Configuration.GetSection("MongoDB").Get(); - string connectionString = mongoConfig.GetConnectionString("smapi"); + string connectionString = mongoConfig.GetConnectionString(); - services.AddSingleton(serv => new MongoClient(connectionString).GetDatabase("smapi")); + services.AddSingleton(serv => new MongoClient(connectionString).GetDatabase(mongoConfig.Database)); services.AddSingleton(serv => new WikiCacheRepository(serv.GetService())); } } diff --git a/src/SMAPI.Web/appsettings.Development.json b/src/SMAPI.Web/appsettings.Development.json index 9b0ec535..3b7ed8bd 100644 --- a/src/SMAPI.Web/appsettings.Development.json +++ b/src/SMAPI.Web/appsettings.Development.json @@ -36,6 +36,7 @@ "MongoDB": { "Host": "localhost", "Username": null, - "Password": null + "Password": null, + "Database": "smapi-edge" } } diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json index 65ccea75..532ea017 100644 --- a/src/SMAPI.Web/appsettings.json +++ b/src/SMAPI.Web/appsettings.json @@ -50,7 +50,8 @@ "MongoDB": { "Host": null, // see top note "Username": null, // see top note - "Password": null // see top note + "Password": null, // see top note + "Database": null // see top note }, "ModCompatibilityList": { -- cgit From a731f5ea9a75aad682c261d747a5d9c71b3d2773 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 7 Jul 2019 00:55:10 -0400 Subject: use better index type (#651) --- docs/technical/web.md | 3 +-- src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheRepository.cs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) (limited to 'src/SMAPI.Web/Framework') diff --git a/docs/technical/web.md b/docs/technical/web.md index f1cb755b..50799e00 100644 --- a/docs/technical/web.md +++ b/docs/technical/web.md @@ -127,5 +127,4 @@ Initial setup: To deploy updates: 1. Deploy the web project using [AWS Toolkit for Visual Studio](https://aws.amazon.com/visualstudio/). -2. If the MongoDB schema changed, delete the collections in the MongoDB database. (They'll be - recreated automatically.) +2. If the MongoDB schema changed, delete the MongoDB database. (It'll be recreated automatically.) diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheRepository.cs index 5b907462..1ae9d38f 100644 --- a/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheRepository.cs +++ b/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheRepository.cs @@ -32,7 +32,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki this.WikiMods = database.GetCollection("wiki-mods"); // add indexes if needed - this.WikiMods.Indexes.CreateOne(new CreateIndexModel(Builders.IndexKeys.Text(p => p.ID))); + this.WikiMods.Indexes.CreateOne(new CreateIndexModel(Builders.IndexKeys.Ascending(p => p.ID))); } /// Get the cached wiki metadata. -- cgit From 48f211f5447ec7580b9f9bba63eab0ec6b1ec5c7 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 14 Jul 2019 17:49:33 -0400 Subject: add metadata links to mod compatibility list --- docs/release-notes.md | 4 ++++ src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs | 10 ++++++++++ src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs | 6 ++++++ src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs | 5 +++++ src/SMAPI.Web/ViewModels/ModModel.cs | 5 +++++ src/SMAPI.Web/Views/Mods/Index.cshtml | 9 ++++++++- 6 files changed, 38 insertions(+), 1 deletion(-) (limited to 'src/SMAPI.Web/Framework') diff --git a/docs/release-notes.md b/docs/release-notes.md index 404424e7..c07ae750 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -24,6 +24,10 @@ These changes have not been released yet. * Fixed map reloads not updating door warps. * Fixed outdoor tilesheets being seasonalised when added to an indoor location. +* For the mod compatibility list: + * Clicking a mod link now automatically adds it to the visible mods when the list is filtered. + * Added metadata links to advanced info, if any. + * For modders: * Mods are now loaded much earlier in the game launch. This lets mods intercept any content asset, but the game is not fully initialised when `Entry` is called (use the `GameLaunched` event if you need to run code when the game is initialised). * Added support for content pack translations. diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs index 3e9b8ea6..24aea5e8 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs @@ -127,6 +127,15 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki } } + // parse links + List> metadataLinks = new List>(); + foreach (HtmlNode linkElement in node.Descendants("td").Last().Descendants("a").Skip(1)) // skip anchor link + { + string text = linkElement.InnerText.Trim(); + Uri url = new Uri(linkElement.GetAttributeValue("href", "")); + metadataLinks.Add(Tuple.Create(url, text)); + } + // yield model yield return new WikiModEntry { @@ -143,6 +152,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki Compatibility = compatibility, BetaCompatibility = betaCompatibility, Warnings = warnings, + MetadataLinks = metadataLinks.ToArray(), Anchor = anchor }; } diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs index cf416cc6..556796c5 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs @@ -1,3 +1,6 @@ +using System; +using System.Collections.Generic; + namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki { /// A mod entry in the wiki list. @@ -48,6 +51,9 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki /// The human-readable warnings for players about this mod. public string[] Warnings { get; set; } + /// Extra metadata links (usually for open pull requests). + public Tuple[] MetadataLinks { get; set; } + /// The link anchor for the mod entry in the wiki compatibility list. public string Anchor { get; set; } } diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs b/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs index f4331f8d..850272eb 100644 --- a/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs +++ b/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs @@ -58,6 +58,9 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki /// The human-readable warnings for players about this mod. public string[] Warnings { get; set; } + /// Extra metadata links (usually for open pull requests). + public Tuple[] MetadataLinks { get; set; } + /// The link anchor for the mod entry in the wiki compatibility list. public string Anchor { get; set; } @@ -122,6 +125,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki this.CustomSourceUrl = mod.CustomSourceUrl; this.CustomUrl = mod.CustomUrl; this.ContentPackFor = mod.ContentPackFor; + this.MetadataLinks = mod.MetadataLinks; this.Warnings = mod.Warnings; this.Anchor = mod.Anchor; @@ -156,6 +160,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki CustomUrl = this.CustomUrl, ContentPackFor = this.ContentPackFor, Warnings = this.Warnings, + MetadataLinks = this.MetadataLinks, Anchor = this.Anchor, // stable compatibility diff --git a/src/SMAPI.Web/ViewModels/ModModel.cs b/src/SMAPI.Web/ViewModels/ModModel.cs index 8668f67b..b57e03f8 100644 --- a/src/SMAPI.Web/ViewModels/ModModel.cs +++ b/src/SMAPI.Web/ViewModels/ModModel.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; @@ -37,6 +38,9 @@ namespace StardewModdingAPI.Web.ViewModels /// The human-readable warnings for players about this mod. public string[] Warnings { get; set; } + /// Extra metadata links (usually for open pull requests). + public Tuple[] MetadataLinks { get; set; } + /// A unique identifier for the mod that can be used in an anchor URL. public string Slug { get; set; } @@ -61,6 +65,7 @@ namespace StardewModdingAPI.Web.ViewModels this.BetaCompatibility = entry.BetaCompatibility != null ? new ModCompatibilityModel(entry.BetaCompatibility) : null; this.ModPages = this.GetModPageUrls(entry).ToArray(); this.Warnings = entry.Warnings; + this.MetadataLinks = entry.MetadataLinks; this.Slug = entry.Anchor; } diff --git a/src/SMAPI.Web/Views/Mods/Index.cshtml b/src/SMAPI.Web/Views/Mods/Index.cshtml index 8293fbe2..2bc135c3 100644 --- a/src/SMAPI.Web/Views/Mods/Index.cshtml +++ b/src/SMAPI.Web/Views/Mods/Index.cshtml @@ -92,7 +92,14 @@ no source - # + + # + + + + -- cgit From 1bf399ec23368a0666203298768a4b24b63d6a88 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 14 Jul 2019 22:27:00 -0400 Subject: add dev note field to compatibility list --- docs/release-notes.md | 2 +- src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs | 2 ++ src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs | 3 +++ src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs | 5 +++++ src/SMAPI.Web/ViewModels/ModModel.cs | 4 ++++ src/SMAPI.Web/Views/Mods/Index.cshtml | 2 ++ 6 files changed, 17 insertions(+), 1 deletion(-) (limited to 'src/SMAPI.Web/Framework') diff --git a/docs/release-notes.md b/docs/release-notes.md index c07ae750..bc5e5a89 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -26,7 +26,7 @@ These changes have not been released yet. * For the mod compatibility list: * Clicking a mod link now automatically adds it to the visible mods when the list is filtered. - * Added metadata links to advanced info, if any. + * Added metadata links and dev notes (if any) to advanced info. * For modders: * Mods are now loaded much earlier in the game launch. This lets mods intercept any content asset, but the game is not fully initialised when `Entry` is called (use the `GameLaunched` event if you need to run code when the game is initialised). diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs index 24aea5e8..ab6a2517 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs @@ -99,6 +99,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki string customUrl = this.GetAttribute(node, "data-url"); string anchor = this.GetAttribute(node, "id"); string contentPackFor = this.GetAttribute(node, "data-content-pack-for"); + string devNote = this.GetAttribute(node, "data-dev-note"); // parse stable compatibility WikiCompatibilityInfo compatibility = new WikiCompatibilityInfo @@ -153,6 +154,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki BetaCompatibility = betaCompatibility, Warnings = warnings, MetadataLinks = metadataLinks.ToArray(), + DevNote = devNote, Anchor = anchor }; } diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs index 556796c5..fff86891 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs @@ -54,6 +54,9 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki /// Extra metadata links (usually for open pull requests). public Tuple[] MetadataLinks { get; set; } + /// Special notes intended for developers who maintain unofficial updates or submit pull requests. + public string DevNote { get; set; } + /// The link anchor for the mod entry in the wiki compatibility list. public string Anchor { get; set; } } diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs b/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs index 850272eb..bf1e2be2 100644 --- a/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs +++ b/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs @@ -61,6 +61,9 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki /// Extra metadata links (usually for open pull requests). public Tuple[] MetadataLinks { get; set; } + /// Special notes intended for developers who maintain unofficial updates or submit pull requests. + public string DevNote { get; set; } + /// The link anchor for the mod entry in the wiki compatibility list. public string Anchor { get; set; } @@ -127,6 +130,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki this.ContentPackFor = mod.ContentPackFor; this.MetadataLinks = mod.MetadataLinks; this.Warnings = mod.Warnings; + this.DevNote = mod.DevNote; this.Anchor = mod.Anchor; // stable compatibility @@ -161,6 +165,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki ContentPackFor = this.ContentPackFor, Warnings = this.Warnings, MetadataLinks = this.MetadataLinks, + DevNote = this.DevNote, Anchor = this.Anchor, // stable compatibility diff --git a/src/SMAPI.Web/ViewModels/ModModel.cs b/src/SMAPI.Web/ViewModels/ModModel.cs index b57e03f8..5a343640 100644 --- a/src/SMAPI.Web/ViewModels/ModModel.cs +++ b/src/SMAPI.Web/ViewModels/ModModel.cs @@ -41,6 +41,9 @@ namespace StardewModdingAPI.Web.ViewModels /// Extra metadata links (usually for open pull requests). public Tuple[] MetadataLinks { get; set; } + /// Special notes intended for developers who maintain unofficial updates or submit pull requests. + public string DevNote { get; set; } + /// A unique identifier for the mod that can be used in an anchor URL. public string Slug { get; set; } @@ -66,6 +69,7 @@ namespace StardewModdingAPI.Web.ViewModels this.ModPages = this.GetModPageUrls(entry).ToArray(); this.Warnings = entry.Warnings; this.MetadataLinks = entry.MetadataLinks; + this.DevNote = entry.DevNote; this.Slug = entry.Anchor; } diff --git a/src/SMAPI.Web/Views/Mods/Index.cshtml b/src/SMAPI.Web/Views/Mods/Index.cshtml index 2bc135c3..2d45a64d 100644 --- a/src/SMAPI.Web/Views/Mods/Index.cshtml +++ b/src/SMAPI.Web/Views/Mods/Index.cshtml @@ -98,6 +98,8 @@ + + [dev note] -- cgit From be1f09f5f9d3318a4bbdf593f671f4eacdd4395d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 18 Jul 2019 15:13:29 -0400 Subject: update obsolete code --- .../Framework/Clients/Pastebin/PastebinClient.cs | 32 ++++++---------------- src/SMAPI.Web/Startup.cs | 8 ++---- 2 files changed, 10 insertions(+), 30 deletions(-) (limited to 'src/SMAPI.Web/Framework') diff --git a/src/SMAPI.Web/Framework/Clients/Pastebin/PastebinClient.cs b/src/SMAPI.Web/Framework/Clients/Pastebin/PastebinClient.cs index 12c3e83f..1e46f2dc 100644 --- a/src/SMAPI.Web/Framework/Clients/Pastebin/PastebinClient.cs +++ b/src/SMAPI.Web/Framework/Clients/Pastebin/PastebinClient.cs @@ -1,11 +1,8 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; -using System.Text; using System.Threading.Tasks; -using System.Web; using Pathoschild.Http.Client; namespace StardewModdingAPI.Web.Framework.Clients.Pastebin @@ -82,15 +79,15 @@ namespace StardewModdingAPI.Web.Framework.Clients.Pastebin // post to API string response = await this.Client .PostAsync("api/api_post.php") - .WithBodyContent(this.GetFormUrlEncodedContent(new Dictionary + .WithBody(p => p.FormUrlEncoded(new { - ["api_option"] = "paste", - ["api_user_key"] = this.UserKey, - ["api_dev_key"] = this.DevKey, - ["api_paste_private"] = "1", // unlisted - ["api_paste_name"] = $"SMAPI log {DateTime.UtcNow:s}", - ["api_paste_expire_date"] = "N", // never expire - ["api_paste_code"] = content + api_option = "paste", + api_user_key = this.UserKey, + api_dev_key = this.DevKey, + api_paste_private = 1, // unlisted + api_paste_name = $"SMAPI log {DateTime.UtcNow:s}", + api_paste_expire_date = "N", // never expire + api_paste_code = content })) .AsString(); @@ -117,18 +114,5 @@ namespace StardewModdingAPI.Web.Framework.Clients.Pastebin { this.Client.Dispose(); } - - - /********* - ** Private methods - *********/ - /// Build an HTTP content body with form-url-encoded content. - /// The content to encode. - /// This bypasses an issue where restricts the body length to the maximum size of a URL, which isn't applicable here. - private HttpContent GetFormUrlEncodedContent(IDictionary data) - { - string body = string.Join("&", from arg in data select $"{HttpUtility.UrlEncode(arg.Key)}={HttpUtility.UrlEncode(arg.Value)}"); - return new StringContent(body, Encoding.UTF8, "application/x-www-form-urlencoded"); - } } } diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index 315f5f88..7beb0bcc 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -5,7 +5,6 @@ using Microsoft.AspNetCore.Rewrite; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using MongoDB.Driver; using Newtonsoft.Json; using StardewModdingAPI.Toolkit.Serialisation; @@ -57,6 +56,7 @@ namespace StardewModdingAPI.Web .Configure(this.Configuration.GetSection("Site")) .Configure(this.Configuration.GetSection("MongoDB")) .Configure(options => options.ConstraintMap.Add("semanticVersion", typeof(VersionConstraint))) + .AddLogging() .AddMemoryCache() .AddMvc() .ConfigureApplicationPartManager(manager => manager.FeatureProviders.Add(new InternalControllerFeatureProvider())) @@ -125,12 +125,8 @@ namespace StardewModdingAPI.Web /// The method called by the runtime to configure the HTTP request pipeline. /// The application builder. /// The hosting environment. - /// The logger factory. - public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + public void Configure(IApplicationBuilder app, IHostingEnvironment env) { - loggerFactory.AddConsole(this.Configuration.GetSection("Logging")); - loggerFactory.AddDebug(); - if (env.IsDevelopment()) app.UseDeveloperExceptionPage(); -- cgit From ce6cedaf4be53d52f2e558055b91e515b92e4c83 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 19 Jul 2019 13:15:45 -0400 Subject: add background fetch for mod compatibility list (#651) --- docs/release-notes.md | 2 + src/SMAPI.Web/BackgroundService.cs | 92 +++++++++++ src/SMAPI.Web/Controllers/ModsController.cs | 33 ++-- .../Framework/Caching/BaseCacheRepository.cs | 6 +- .../Framework/Caching/Wiki/CachedWikiMetadata.cs | 2 +- .../Framework/Caching/Wiki/IWikiCacheRepository.cs | 4 +- .../ConfigModels/BackgroundServicesConfig.cs | 12 ++ .../ConfigModels/ModCompatibilityListConfig.cs | 6 +- src/SMAPI.Web/SMAPI.Web.csproj | 2 + src/SMAPI.Web/Startup.cs | 48 ++++-- src/SMAPI.Web/ViewModels/ModListModel.cs | 19 ++- src/SMAPI.Web/Views/Mods/Index.cshtml | 172 +++++++++++---------- src/SMAPI.Web/appsettings.Development.json | 7 - src/SMAPI.Web/appsettings.json | 9 +- src/SMAPI.Web/wwwroot/Content/css/mods.css | 57 ++++--- 15 files changed, 317 insertions(+), 154 deletions(-) create mode 100644 src/SMAPI.Web/BackgroundService.cs create mode 100644 src/SMAPI.Web/Framework/ConfigModels/BackgroundServicesConfig.cs (limited to 'src/SMAPI.Web/Framework') diff --git a/docs/release-notes.md b/docs/release-notes.md index ea988fe5..9c3e3d28 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -25,6 +25,8 @@ These changes have not been released yet. * Fixed outdoor tilesheets being seasonalised when added to an indoor location. * For the mod compatibility list: + * Now loads faster (since data is fetched in a background service). + * Now continues working with cached data when the wiki is offline. * Clicking a mod link now automatically adds it to the visible mods when the list is filtered. * Added metadata links and dev notes (if any) to advanced info. diff --git a/src/SMAPI.Web/BackgroundService.cs b/src/SMAPI.Web/BackgroundService.cs new file mode 100644 index 00000000..2ccfd5f7 --- /dev/null +++ b/src/SMAPI.Web/BackgroundService.cs @@ -0,0 +1,92 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Hangfire; +using Microsoft.Extensions.Hosting; +using StardewModdingAPI.Toolkit; +using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; +using StardewModdingAPI.Web.Framework.Caching.Wiki; + +namespace StardewModdingAPI.Web +{ + /// A hosted service which runs background data updates. + /// Task methods need to be static, since otherwise Hangfire will try to serialise the entire instance. + internal class BackgroundService : IHostedService, IDisposable + { + /********* + ** Fields + *********/ + /// The background task server. + private static BackgroundJobServer JobServer; + + /// The cache in which to store mod metadata. + private static IWikiCacheRepository WikiCache; + + + /********* + ** Public methods + *********/ + /**** + ** Hosted service + ****/ + /// Construct an instance. + /// The cache in which to store mod metadata. + public BackgroundService(IWikiCacheRepository wikiCache) + { + BackgroundService.WikiCache = wikiCache; + } + + /// Start the service. + /// Tracks whether the start process has been aborted. + public Task StartAsync(CancellationToken cancellationToken) + { + this.TryInit(); + + // set startup tasks + BackgroundJob.Enqueue(() => BackgroundService.UpdateWikiAsync()); + + // set recurring tasks + RecurringJob.AddOrUpdate(() => BackgroundService.UpdateWikiAsync(), "*/10 * * * *"); + + return Task.CompletedTask; + } + + /// Triggered when the application host is performing a graceful shutdown. + /// Tracks whether the shutdown process should no longer be graceful. + public async Task StopAsync(CancellationToken cancellationToken) + { + if (BackgroundService.JobServer != null) + await BackgroundService.JobServer.WaitForShutdownAsync(cancellationToken); + } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public void Dispose() + { + BackgroundService.JobServer?.Dispose(); + } + + /**** + ** Tasks + ****/ + /// Update the cached wiki metadata. + public static async Task UpdateWikiAsync() + { + WikiModList wikiCompatList = await new ModToolkit().GetWikiCompatibilityListAsync(); + BackgroundService.WikiCache.SaveWikiData(wikiCompatList.StableVersion, wikiCompatList.BetaVersion, wikiCompatList.Mods, out _, out _); + } + + + /********* + ** Private method + *********/ + /// Initialise the background service if it's not already initialised. + /// The background service is already initialised. + private void TryInit() + { + if (BackgroundService.JobServer != null) + throw new InvalidOperationException("The scheduler service is already started."); + + BackgroundService.JobServer = new BackgroundJobServer(); + } + } +} diff --git a/src/SMAPI.Web/Controllers/ModsController.cs b/src/SMAPI.Web/Controllers/ModsController.cs index b6040e06..b621ded0 100644 --- a/src/SMAPI.Web/Controllers/ModsController.cs +++ b/src/SMAPI.Web/Controllers/ModsController.cs @@ -1,9 +1,7 @@ using System.Linq; using System.Text.RegularExpressions; -using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; -using StardewModdingAPI.Toolkit; using StardewModdingAPI.Web.Framework.Caching.Wiki; using StardewModdingAPI.Web.Framework.ConfigModels; using StardewModdingAPI.Web.ViewModels; @@ -19,8 +17,8 @@ namespace StardewModdingAPI.Web.Controllers /// The cache in which to store mod metadata. private readonly IWikiCacheRepository Cache; - /// The number of minutes successful update checks should be cached before refetching them. - private readonly int CacheMinutes; + /// The number of minutes before which wiki data should be considered old. + private readonly int StaleMinutes; /********* @@ -34,15 +32,15 @@ namespace StardewModdingAPI.Web.Controllers ModCompatibilityListConfig config = configProvider.Value; this.Cache = cache; - this.CacheMinutes = config.CacheMinutes; + this.StaleMinutes = config.StaleMinutes; } /// Display information for all mods. [HttpGet] [Route("mods")] - public async Task Index() + public ViewResult Index() { - return this.View("Index", await this.FetchDataAsync()); + return this.View("Index", this.FetchData()); } @@ -50,25 +48,22 @@ namespace StardewModdingAPI.Web.Controllers ** Private methods *********/ /// Asynchronously fetch mod metadata from the wiki. - public async Task FetchDataAsync() + public ModListModel FetchData() { - // refresh cache - CachedWikiMod[] mods; - if (!this.Cache.TryGetWikiMetadata(out CachedWikiMetadata metadata) || this.Cache.IsStale(metadata.LastUpdated, this.CacheMinutes)) - { - var wikiCompatList = await new ModToolkit().GetWikiCompatibilityListAsync(); - this.Cache.SaveWikiData(wikiCompatList.StableVersion, wikiCompatList.BetaVersion, wikiCompatList.Mods, out metadata, out mods); - } - else - mods = this.Cache.GetWikiMods().ToArray(); + // fetch cached data + if (!this.Cache.TryGetWikiMetadata(out CachedWikiMetadata metadata)) + return new ModListModel(); // build model return new ModListModel( stableVersion: metadata.StableVersion, betaVersion: metadata.BetaVersion, - mods: mods + mods: this.Cache + .GetWikiMods() .Select(mod => new ModModel(mod.GetModel())) - .OrderBy(p => Regex.Replace(p.Name.ToLower(), "[^a-z0-9]", "")) // ignore case, spaces, and special characters when sorting + .OrderBy(p => Regex.Replace(p.Name.ToLower(), "[^a-z0-9]", "")), // ignore case, spaces, and special characters when sorting + lastUpdated: metadata.LastUpdated, + isStale: this.Cache.IsStale(metadata.LastUpdated, this.StaleMinutes) ); } } diff --git a/src/SMAPI.Web/Framework/Caching/BaseCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/BaseCacheRepository.cs index 904455c5..f5354b93 100644 --- a/src/SMAPI.Web/Framework/Caching/BaseCacheRepository.cs +++ b/src/SMAPI.Web/Framework/Caching/BaseCacheRepository.cs @@ -10,10 +10,10 @@ namespace StardewModdingAPI.Web.Framework.Caching *********/ /// Whether cached data is stale. /// The date when the data was updated. - /// The age in minutes before data is considered stale. - public bool IsStale(DateTimeOffset lastUpdated, int cacheMinutes) + /// The age in minutes before data is considered stale. + public bool IsStale(DateTimeOffset lastUpdated, int staleMinutes) { - return lastUpdated < DateTimeOffset.UtcNow.AddMinutes(-cacheMinutes); + return lastUpdated < DateTimeOffset.UtcNow.AddMinutes(-staleMinutes); } } } diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMetadata.cs b/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMetadata.cs index de1ea9db..4d6b4b10 100644 --- a/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMetadata.cs +++ b/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMetadata.cs @@ -37,7 +37,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki { this.StableVersion = stableVersion; this.BetaVersion = betaVersion; - this.LastUpdated = DateTimeOffset.UtcNow;; + this.LastUpdated = DateTimeOffset.UtcNow; } } } diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs index d319db69..6031123d 100644 --- a/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs +++ b/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs @@ -17,8 +17,8 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki /// Whether cached data is stale. /// The date when the data was updated. - /// The age in minutes before data is considered stale. - bool IsStale(DateTimeOffset lastUpdated, int cacheMinutes); + /// The age in minutes before data is considered stale. + bool IsStale(DateTimeOffset lastUpdated, int staleMinutes); /// Get the cached wiki mods. /// A filter to apply, if any. diff --git a/src/SMAPI.Web/Framework/ConfigModels/BackgroundServicesConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/BackgroundServicesConfig.cs new file mode 100644 index 00000000..de871c9a --- /dev/null +++ b/src/SMAPI.Web/Framework/ConfigModels/BackgroundServicesConfig.cs @@ -0,0 +1,12 @@ +namespace StardewModdingAPI.Web.Framework.ConfigModels +{ + /// The config settings for background services. + internal class BackgroundServicesConfig + { + /********* + ** Accessors + *********/ + /// Whether to enable background update services. + public bool Enabled { get; set; } + } +} diff --git a/src/SMAPI.Web/Framework/ConfigModels/ModCompatibilityListConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ModCompatibilityListConfig.cs index d9ac9f02..24b540cd 100644 --- a/src/SMAPI.Web/Framework/ConfigModels/ModCompatibilityListConfig.cs +++ b/src/SMAPI.Web/Framework/ConfigModels/ModCompatibilityListConfig.cs @@ -1,12 +1,12 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels { - /// The config settings for mod compatibility list. + /// The config settings for the mod compatibility list. internal class ModCompatibilityListConfig { /********* ** Accessors *********/ - /// The number of minutes data from the wiki should be cached before refetching it. - public int CacheMinutes { get; set; } + /// The number of minutes before which wiki data should be considered old. + public int StaleMinutes { get; set; } } } diff --git a/src/SMAPI.Web/SMAPI.Web.csproj b/src/SMAPI.Web/SMAPI.Web.csproj index 2f90389b..d53914d5 100644 --- a/src/SMAPI.Web/SMAPI.Web.csproj +++ b/src/SMAPI.Web/SMAPI.Web.csproj @@ -16,6 +16,8 @@ + + diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index 7beb0bcc..bdfa5ed9 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using Hangfire; +using Hangfire.Mongo; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Rewrite; @@ -49,12 +51,13 @@ namespace StardewModdingAPI.Web /// The service injection container. public void ConfigureServices(IServiceCollection services) { - // init configuration + // init basic services services + .Configure(this.Configuration.GetSection("BackgroundServices")) .Configure(this.Configuration.GetSection("ModCompatibilityList")) .Configure(this.Configuration.GetSection("ModUpdateCheck")) - .Configure(this.Configuration.GetSection("Site")) .Configure(this.Configuration.GetSection("MongoDB")) + .Configure(this.Configuration.GetSection("Site")) .Configure(options => options.ConstraintMap.Add("semanticVersion", typeof(VersionConstraint))) .AddLogging() .AddMemoryCache() @@ -69,6 +72,33 @@ namespace StardewModdingAPI.Web options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; }); + // init background service + { + BackgroundServicesConfig config = this.Configuration.GetSection("BackgroundServices").Get(); + if (config.Enabled) + services.AddHostedService(); + } + + // init MongoDB + MongoDbConfig mongoConfig = this.Configuration.GetSection("MongoDB").Get(); + string mongoConnectionStr = mongoConfig.GetConnectionString(); + services.AddSingleton(serv => new MongoClient(mongoConnectionStr).GetDatabase(mongoConfig.Database)); + services.AddSingleton(serv => new WikiCacheRepository(serv.GetService())); + + // init Hangfire (needs MongoDB) + services + .AddHangfire(config => + { + config + .SetDataCompatibilityLevel(CompatibilityLevel.Version_170) + .UseSimpleAssemblyNameTypeSerializer() + .UseRecommendedSerializerSettings() + .UseMongoStorage(mongoConnectionStr, $"{mongoConfig.Database}-hangfire", new MongoStorageOptions + { + MigrationOptions = new MongoMigrationOptions(MongoMigrationStrategy.Drop) + }); + }); + // init API clients { ApiClientsConfig api = this.Configuration.GetSection("ApiClients").Get(); @@ -111,15 +141,6 @@ namespace StardewModdingAPI.Web devKey: api.PastebinDevKey )); } - - // init MongoDB - { - MongoDbConfig mongoConfig = this.Configuration.GetSection("MongoDB").Get(); - string connectionString = mongoConfig.GetConnectionString(); - - services.AddSingleton(serv => new MongoClient(connectionString).GetDatabase(mongoConfig.Database)); - services.AddSingleton(serv => new WikiCacheRepository(serv.GetService())); - } } /// The method called by the runtime to configure the HTTP request pipeline. @@ -127,9 +148,9 @@ namespace StardewModdingAPI.Web /// The hosting environment. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { + // basic config if (env.IsDevelopment()) app.UseDeveloperExceptionPage(); - app .UseCors(policy => policy .AllowAnyHeader() @@ -140,6 +161,9 @@ namespace StardewModdingAPI.Web .UseRewriter(this.GetRedirectRules()) .UseStaticFiles() // wwwroot folder .UseMvc(); + + // config Hangfire + app.UseHangfireDashboard("/tasks"); } diff --git a/src/SMAPI.Web/ViewModels/ModListModel.cs b/src/SMAPI.Web/ViewModels/ModListModel.cs index 3b87d393..ff7513bc 100644 --- a/src/SMAPI.Web/ViewModels/ModListModel.cs +++ b/src/SMAPI.Web/ViewModels/ModListModel.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; @@ -18,19 +19,35 @@ namespace StardewModdingAPI.Web.ViewModels /// The mods to display. public ModModel[] Mods { get; set; } + /// When the data was last updated. + public DateTimeOffset LastUpdated { get; set; } + + /// Whether the data hasn't been updated in a while. + public bool IsStale { get; set; } + + /// Whether the mod metadata is available. + public bool HasData => this.Mods != null; + /********* ** Public methods *********/ + /// Construct an empty instance. + public ModListModel() { } + /// Construct an instance. /// The current stable version of the game. /// The current beta version of the game (if any). /// The mods to display. - public ModListModel(string stableVersion, string betaVersion, IEnumerable mods) + /// When the data was last updated. + /// Whether the data hasn't been updated in a while. + public ModListModel(string stableVersion, string betaVersion, IEnumerable mods, DateTimeOffset lastUpdated, bool isStale) { this.StableVersion = stableVersion; this.BetaVersion = betaVersion; this.Mods = mods.ToArray(); + this.LastUpdated = lastUpdated; + this.IsStale = isStale; } } } diff --git a/src/SMAPI.Web/Views/Mods/Index.cshtml b/src/SMAPI.Web/Views/Mods/Index.cshtml index 2d45a64d..aa7c0678 100644 --- a/src/SMAPI.Web/Views/Mods/Index.cshtml +++ b/src/SMAPI.Web/Views/Mods/Index.cshtml @@ -18,92 +18,104 @@ } -
-
-

This page shows all known SMAPI mods and (incompatible) content packs, whether they work with the latest versions of Stardew Valley and SMAPI, and how to fix them if not. If a mod doesn't work after following the instructions below, check the troubleshooting guide or ask for help.

+@if (!Model.HasData) +{ +
↻ The mod data hasn't been fetched yet; please try again in a few minutes.
+} +else +{ + @if (Model.IsStale) + { +
Showing data from @(Math.Round((DateTimeOffset.UtcNow - Model.LastUpdated).TotalMinutes)) minutes ago. (Couldn't fetch newer data; the wiki API may be offline.)
+ } -

The list is updated every few days (you can help update it!). It doesn't include XNB mods (see using XNB mods on the wiki instead) or compatible content packs.

+
+
+

This page shows all known SMAPI mods and (incompatible) content packs, whether they work with the latest versions of Stardew Valley and SMAPI, and how to fix them if not. If a mod doesn't work after following the instructions below, check the troubleshooting guide or ask for help.

- @if (Model.BetaVersion != null) - { -

Note: "SDV @Model.BetaVersion only" lines are for an unreleased version of the game, not the stable version most players have. If a mod doesn't have that line, the info applies to both versions of the game.

- } -
+

The list is updated every few days (you can help update it!). It doesn't include XNB mods (see using XNB mods on the wiki instead) or compatible content packs.

-
-
- - + @if (Model.BetaVersion != null) + { +

Note: "SDV @Model.BetaVersion only" lines are for an unreleased version of the game, not the stable version most players have. If a mod doesn't have that line, the info applies to both versions of the game.

+ }
-
- - -
-
- {{filterGroup.label}}: + +
+
+ + +
+
+ + +
+
+ {{filterGroup.label}}: +
-
-
-
- {{visibleStats.total}} mods shown ({{Math.round((visibleStats.compatible + visibleStats.workaround) / visibleStats.total * 100)}}% compatible or have a workaround, {{Math.round((visibleStats.soon + visibleStats.broken) / visibleStats.total * 100)}}% broken, {{Math.round(visibleStats.abandoned / visibleStats.total * 100)}}% obsolete). +
+
+ {{visibleStats.total}} mods shown ({{Math.round((visibleStats.compatible + visibleStats.workaround) / visibleStats.total * 100)}}% compatible or have a workaround, {{Math.round((visibleStats.soon + visibleStats.broken) / visibleStats.total * 100)}}% broken, {{Math.round(visibleStats.abandoned / visibleStats.total * 100)}}% obsolete). +
+ No matching mods found.
- No matching mods found. -
- - - - - - - - - - - - - - - - - - - - - + + + + + + + +
mod namelinksauthorcompatibilitybroke incode 
- {{mod.Name}} - (aka {{mod.AlternateNames}}) - - {{mod.Author}} - (aka {{mod.AlternateAuthors}}) - -
-
- SDV @Model.BetaVersion only: - -
-
⚠ {{warning}}
-
- source - no source - - - # - - - - [dev note] + + + + + + + + + + + + + + + + - - -
mod namelinksauthorcompatibilitybroke incode 
+ {{mod.Name}} + (aka {{mod.AlternateNames}}) +
- +
+ {{mod.Author}} + (aka {{mod.AlternateAuthors}}) + +
+
+ SDV @Model.BetaVersion only: + +
+
⚠ {{warning}}
+
+ source + no source + + + # + + + + [dev note] + + +
+
+} diff --git a/src/SMAPI.Web/appsettings.Development.json b/src/SMAPI.Web/appsettings.Development.json index c50557b5..5856b96c 100644 --- a/src/SMAPI.Web/appsettings.Development.json +++ b/src/SMAPI.Web/appsettings.Development.json @@ -8,13 +8,6 @@ */ { - "Logging": { - "IncludeScopes": false, - "LogLevel": { - "Default": "Warning" - } - }, - "Site": { "RootUrl": "http://localhost:59482/", "ModListUrl": "http://localhost:59482/mods/", diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json index 532ea017..ea7e9cd2 100644 --- a/src/SMAPI.Web/appsettings.json +++ b/src/SMAPI.Web/appsettings.json @@ -10,7 +10,8 @@ "Logging": { "IncludeScopes": false, "LogLevel": { - "Default": "Warning" + "Default": "Warning", + "Hangfire": "Information" } }, @@ -55,7 +56,11 @@ }, "ModCompatibilityList": { - "CacheMinutes": 10 + "StaleMinutes": 15 + }, + + "BackgroundServices": { + "Enabled": true }, "ModUpdateCheck": { diff --git a/src/SMAPI.Web/wwwroot/Content/css/mods.css b/src/SMAPI.Web/wwwroot/Content/css/mods.css index fc5fff47..1c2b8056 100644 --- a/src/SMAPI.Web/wwwroot/Content/css/mods.css +++ b/src/SMAPI.Web/wwwroot/Content/css/mods.css @@ -15,30 +15,6 @@ border: 3px solid darkgreen; } -table.wikitable { - background-color:#f8f9fa; - color:#222; - border:1px solid #a2a9b1; - border-collapse:collapse -} - -table.wikitable > tr > th, -table.wikitable > tr > td, -table.wikitable > * > tr > th, -table.wikitable > * > tr > td { - border:1px solid #a2a9b1; - padding:0.2em 0.4em -} - -table.wikitable > tr > th, -table.wikitable > * > tr > th { - background-color:#eaecf0; -} - -table.wikitable > caption { - font-weight:bold -} - #options { margin-bottom: 1em; } @@ -73,6 +49,39 @@ table.wikitable > caption { opacity: 0.5; } +div.error { + padding: 2em 0; + color: red; + font-weight: bold; +} + +/********* +** Mod list +*********/ +table.wikitable { + background-color:#f8f9fa; + color:#222; + border:1px solid #a2a9b1; + border-collapse:collapse +} + +table.wikitable > tr > th, +table.wikitable > tr > td, +table.wikitable > * > tr > th, +table.wikitable > * > tr > td { + border:1px solid #a2a9b1; + padding:0.2em 0.4em +} + +table.wikitable > tr > th, +table.wikitable > * > tr > th { + background-color:#eaecf0; +} + +table.wikitable > caption { + font-weight:bold +} + #mod-list { font-size: 0.9em; } -- cgit From ec747b518b28184c440dcea7ce74f3e80b627505 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 19 Jul 2019 17:01:22 -0400 Subject: enable readonly access to job dashboard when deployed (#651) --- .../Framework/JobDashboardAuthorizationFilter.cs | 34 ++++++++++++++++++++++ src/SMAPI.Web/Startup.cs | 8 +++-- 2 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 src/SMAPI.Web/Framework/JobDashboardAuthorizationFilter.cs (limited to 'src/SMAPI.Web/Framework') diff --git a/src/SMAPI.Web/Framework/JobDashboardAuthorizationFilter.cs b/src/SMAPI.Web/Framework/JobDashboardAuthorizationFilter.cs new file mode 100644 index 00000000..9471d5fe --- /dev/null +++ b/src/SMAPI.Web/Framework/JobDashboardAuthorizationFilter.cs @@ -0,0 +1,34 @@ +using Hangfire.Dashboard; + +namespace StardewModdingAPI.Web.Framework +{ + /// Authorises requests to access the Hangfire job dashboard. + internal class JobDashboardAuthorizationFilter : IDashboardAuthorizationFilter + { + /********* + ** Fields + *********/ + /// An authorization filter that allows local requests. + private static readonly LocalRequestsOnlyAuthorizationFilter LocalRequestsOnlyFilter = new LocalRequestsOnlyAuthorizationFilter(); + + + /********* + ** Public methods + *********/ + /// Authorise a request. + /// The dashboard context. + public bool Authorize(DashboardContext context) + { + return + context.IsReadOnly // always allow readonly access + || JobDashboardAuthorizationFilter.IsLocalRequest(context); // else allow access from localhost + } + + /// Get whether a request originated from a user on the server machine. + /// The dashboard context. + public static bool IsLocalRequest(DashboardContext context) + { + return JobDashboardAuthorizationFilter.LocalRequestsOnlyFilter.Authorize(context); + } + } +} diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index 552d623c..d0456df3 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -162,8 +162,12 @@ namespace StardewModdingAPI.Web .UseStaticFiles() // wwwroot folder .UseMvc(); - // config Hangfire - app.UseHangfireDashboard("/tasks"); + // enable Hangfire dashboard + app.UseHangfireDashboard("/tasks", new DashboardOptions + { + IsReadOnlyFunc = context => !JobDashboardAuthorizationFilter.IsLocalRequest(context), + Authorization = new[] { new JobDashboardAuthorizationFilter() } + }); } -- cgit From e856d5efebe12b3aa65d5868ea7baa59cc54863d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 24 Jul 2019 18:29:50 -0400 Subject: add remote mod status to update check info (#651) --- docs/release-notes.md | 1 + src/SMAPI.Web/Controllers/ModsApiController.cs | 10 ++++----- .../Framework/Clients/ModDrop/ModDropMod.cs | 3 --- .../ModRepositories/ChucklefishRepository.cs | 6 ++--- .../Framework/ModRepositories/GitHubRepository.cs | 6 ++--- .../Framework/ModRepositories/ModDropRepository.cs | 8 +++---- .../Framework/ModRepositories/ModInfoModel.cs | 26 ++++++++++++---------- .../Framework/ModRepositories/NexusRepository.cs | 8 +++---- .../Framework/ModRepositories/RemoteModStatus.cs | 18 +++++++++++++++ 9 files changed, 51 insertions(+), 35 deletions(-) create mode 100644 src/SMAPI.Web/Framework/ModRepositories/RemoteModStatus.cs (limited to 'src/SMAPI.Web/Framework') diff --git a/docs/release-notes.md b/docs/release-notes.md index abc08e16..4b380564 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -40,6 +40,7 @@ These changes have not been released yet. * The installer now recognises custom game paths stored in `stardewvalley.targets`, if any. * Trace logs for a broken mod now list all detected issues (instead of the first one). * Trace logs when loading mods are now more clear. + * Clarified update-check errors for mods with multiple update keys. * Fixed custom maps loaded from `.xnb` files not having their tilesheet paths automatically adjusted. * Fixed custom maps loaded from the mod folder with tilesheets in a subfolder not working crossplatform. All tilesheet paths are now normalised for the OS automatically. * Removed all deprecated APIs. diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 9cffd3b3..a74d0d8a 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -245,11 +245,11 @@ namespace StardewModdingAPI.Web.Controllers // parse update key UpdateKey parsed = UpdateKey.Parse(updateKey); if (!parsed.LooksValid) - return new ModInfoModel($"The update key '{updateKey}' isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541'."); + return new ModInfoModel().WithError(RemoteModStatus.DoesNotExist, $"The update key '{updateKey}' isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541'."); // get matching repository if (!this.Repositories.TryGetValue(parsed.Repository, out IModRepository repository)) - return new ModInfoModel($"There's no mod site with key '{parsed.Repository}'. Expected one of [{string.Join(", ", this.Repositories.Keys)}]."); + return new ModInfoModel().WithError(RemoteModStatus.DoesNotExist, $"There's no mod site with key '{parsed.Repository}'. Expected one of [{string.Join(", ", this.Repositories.Keys)}]."); // fetch mod info return await this.Cache.GetOrCreateAsync($"{repository.VendorKey}:{parsed.ID}".ToLower(), async entry => @@ -258,11 +258,11 @@ namespace StardewModdingAPI.Web.Controllers if (result.Error == null) { if (result.Version == null) - result.Error = $"The update key '{updateKey}' matches a mod with no version number."; + result.WithError(RemoteModStatus.InvalidData, $"The update key '{updateKey}' matches a mod with no version number."); else if (!Regex.IsMatch(result.Version, this.VersionRegex, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)) - result.Error = $"The update key '{updateKey}' matches a mod with invalid semantic version '{result.Version}'."; + result.WithError(RemoteModStatus.InvalidData, $"The update key '{updateKey}' matches a mod with invalid semantic version '{result.Version}'."); } - entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(result.Error == null ? this.SuccessCacheMinutes : this.ErrorCacheMinutes); + entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(result.Status == RemoteModStatus.TemporaryError ? this.ErrorCacheMinutes : this.SuccessCacheMinutes); return result; }); } diff --git a/src/SMAPI.Web/Framework/Clients/ModDrop/ModDropMod.cs b/src/SMAPI.Web/Framework/Clients/ModDrop/ModDropMod.cs index 291fb353..def79106 100644 --- a/src/SMAPI.Web/Framework/Clients/ModDrop/ModDropMod.cs +++ b/src/SMAPI.Web/Framework/Clients/ModDrop/ModDropMod.cs @@ -17,8 +17,5 @@ namespace StardewModdingAPI.Web.Framework.Clients.ModDrop /// The mod's web URL. public string Url { get; set; } - - /// A user-friendly error which indicates why fetching the mod info failed (if applicable). - public string Error { get; set; } } } diff --git a/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs index 87e29a2f..04c80dd2 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs @@ -32,21 +32,21 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories { // validate ID format if (!uint.TryParse(id, out uint realID)) - return new ModInfoModel($"The value '{id}' isn't a valid Chucklefish mod ID, must be an integer ID."); + return new ModInfoModel().WithError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid Chucklefish mod ID, must be an integer ID."); // fetch info try { var mod = await this.Client.GetModAsync(realID); if (mod == null) - return new ModInfoModel("Found no mod with this ID."); + return new ModInfoModel().WithError(RemoteModStatus.DoesNotExist, "Found no Chucklefish mod with this ID."); // create model return new ModInfoModel(name: mod.Name, version: this.NormaliseVersion(mod.Version), url: mod.Url); } catch (Exception ex) { - return new ModInfoModel(ex.ToString()); + return new ModInfoModel().WithError(RemoteModStatus.TemporaryError, ex.ToString()); } } diff --git a/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs index 14f44dc0..614e00c2 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs @@ -32,7 +32,7 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories { // validate ID format if (!id.Contains("/") || id.IndexOf("/", StringComparison.InvariantCultureIgnoreCase) != id.LastIndexOf("/", StringComparison.InvariantCultureIgnoreCase)) - return new ModInfoModel($"The value '{id}' isn't a valid GitHub mod ID, must be a username and project name like 'Pathoschild/LookupAnything'."); + return new ModInfoModel().WithError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid GitHub mod ID, must be a username and project name like 'Pathoschild/LookupAnything'."); // fetch info try @@ -40,7 +40,7 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories // get latest release (whether preview or stable) GitRelease latest = await this.Client.GetLatestReleaseAsync(id, includePrerelease: true); if (latest == null) - return new ModInfoModel("Found no mod with this ID."); + return new ModInfoModel().WithError(RemoteModStatus.DoesNotExist, "Found no GitHub release for this ID."); // split stable/prerelease if applicable GitRelease preview = null; @@ -59,7 +59,7 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories } catch (Exception ex) { - return new ModInfoModel(ex.ToString()); + return new ModInfoModel().WithError(RemoteModStatus.TemporaryError, ex.ToString()); } } diff --git a/src/SMAPI.Web/Framework/ModRepositories/ModDropRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/ModDropRepository.cs index 1994f515..5703b34e 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/ModDropRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/ModDropRepository.cs @@ -32,21 +32,19 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories { // validate ID format if (!long.TryParse(id, out long modDropID)) - return new ModInfoModel($"The value '{id}' isn't a valid ModDrop mod ID, must be an integer ID."); + return new ModInfoModel().WithError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid ModDrop mod ID, must be an integer ID."); // fetch info try { ModDropMod mod = await this.Client.GetModAsync(modDropID); if (mod == null) - return new ModInfoModel("Found no mod with this ID."); - if (mod.Error != null) - return new ModInfoModel(mod.Error); + return new ModInfoModel().WithError(RemoteModStatus.DoesNotExist, "Found no ModDrop mod with this ID."); return new ModInfoModel(name: mod.Name, version: mod.LatestDefaultVersion?.ToString(), previewVersion: mod.LatestOptionalVersion?.ToString(), url: mod.Url); } catch (Exception ex) { - return new ModInfoModel(ex.ToString()); + return new ModInfoModel().WithError(RemoteModStatus.TemporaryError, ex.ToString()); } } diff --git a/src/SMAPI.Web/Framework/ModRepositories/ModInfoModel.cs b/src/SMAPI.Web/Framework/ModRepositories/ModInfoModel.cs index 18252298..16885bfd 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/ModInfoModel.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/ModInfoModel.cs @@ -9,15 +9,18 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories /// The mod name. public string Name { get; set; } - /// The mod's latest release number. + /// The mod's latest version. public string Version { get; set; } - /// The mod's latest optional release, if newer than . + /// The mod's latest optional or prerelease version, if newer than . public string PreviewVersion { get; set; } /// The mod's web URL. public string Url { get; set; } + /// The mod availability status. + public RemoteModStatus Status { get; set; } = RemoteModStatus.Ok; + /// The error message indicating why the mod is invalid (if applicable). public string Error { get; set; } @@ -26,31 +29,30 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories ** Public methods *********/ /// Construct an empty instance. - public ModInfoModel() - { - // needed for JSON deserialising - } + public ModInfoModel() { } /// Construct an instance. /// The mod name. /// 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 previewVersion = null, string error = null) + public ModInfoModel(string name, string version, string url, string previewVersion = null) { this.Name = name; this.Version = version; this.PreviewVersion = previewVersion; this.Url = url; - this.Error = error; } - /// Construct an instance. - /// The error message indicating why the mod is invalid. - public ModInfoModel(string error) + /// Set a mod error. + /// The mod availability status. + /// The error message indicating why the mod is invalid (if applicable). + public ModInfoModel WithError(RemoteModStatus status, string error) { + this.Status = status; this.Error = error; + + return this; } } } diff --git a/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs index 4c5fe9bf..9679f93e 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs @@ -32,21 +32,21 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories { // validate ID format if (!uint.TryParse(id, out uint nexusID)) - return new ModInfoModel($"The value '{id}' isn't a valid Nexus mod ID, must be an integer ID."); + return new ModInfoModel().WithError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid Nexus mod ID, must be an integer ID."); // fetch info try { NexusMod mod = await this.Client.GetModAsync(nexusID); if (mod == null) - return new ModInfoModel("Found no mod with this ID."); + return new ModInfoModel().WithError(RemoteModStatus.DoesNotExist, "Found no Nexus mod with this ID."); if (mod.Error != null) - return new ModInfoModel(mod.Error); + return new ModInfoModel().WithError(RemoteModStatus.TemporaryError, mod.Error); return new ModInfoModel(name: mod.Name, version: this.NormaliseVersion(mod.Version), previewVersion: mod.LatestFileVersion?.ToString(), url: mod.Url); } catch (Exception ex) { - return new ModInfoModel(ex.ToString()); + return new ModInfoModel().WithError(RemoteModStatus.TemporaryError, ex.ToString()); } } diff --git a/src/SMAPI.Web/Framework/ModRepositories/RemoteModStatus.cs b/src/SMAPI.Web/Framework/ModRepositories/RemoteModStatus.cs new file mode 100644 index 00000000..02876556 --- /dev/null +++ b/src/SMAPI.Web/Framework/ModRepositories/RemoteModStatus.cs @@ -0,0 +1,18 @@ +namespace StardewModdingAPI.Web.Framework.ModRepositories +{ + /// The mod availability status on a remote site. + internal enum RemoteModStatus + { + /// The mod is valid. + Ok, + + /// The mod data was fetched, but the data is not valid (e.g. version isn't semantic). + InvalidData, + + /// The mod does not exist. + DoesNotExist, + + /// The mod was temporarily unavailable (e.g. the site could not be reached or an unknown error occurred). + TemporaryError + } +} -- cgit From f6b336def7b05c3ef202f6c6b5e77dcaa0c498fa Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 24 Jul 2019 18:31:24 -0400 Subject: store DateTimeOffset values in date fields instead of the default array (#651) --- .../Caching/UtcDateTimeOffsetSerializer.cs | 40 ++++++++++++++++++++++ src/SMAPI.Web/Startup.cs | 8 ++++- 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 src/SMAPI.Web/Framework/Caching/UtcDateTimeOffsetSerializer.cs (limited to 'src/SMAPI.Web/Framework') diff --git a/src/SMAPI.Web/Framework/Caching/UtcDateTimeOffsetSerializer.cs b/src/SMAPI.Web/Framework/Caching/UtcDateTimeOffsetSerializer.cs new file mode 100644 index 00000000..ad95a975 --- /dev/null +++ b/src/SMAPI.Web/Framework/Caching/UtcDateTimeOffsetSerializer.cs @@ -0,0 +1,40 @@ +using System; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; + +namespace StardewModdingAPI.Web.Framework.Caching +{ + /// Serialises to a UTC date field instead of the default array. + public class UtcDateTimeOffsetSerializer : StructSerializerBase + { + /********* + ** Fields + *********/ + /// The underlying date serializer. + private static readonly DateTimeSerializer DateTimeSerializer = new DateTimeSerializer(DateTimeKind.Utc, BsonType.DateTime); + + + /********* + ** Public methods + *********/ + /// Deserializes a value. + /// The deserialization context. + /// The deserialization args. + /// A deserialized value. + public override DateTimeOffset Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) + { + DateTime date = UtcDateTimeOffsetSerializer.DateTimeSerializer.Deserialize(context, args); + return new DateTimeOffset(date, TimeSpan.Zero); + } + + /// Serializes a value. + /// The serialization context. + /// The serialization args. + /// The object. + public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, DateTimeOffset value) + { + UtcDateTimeOffsetSerializer.DateTimeSerializer.Serialize(context, args, value.UtcDateTime); + } + } +} diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index d0456df3..caa7b056 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -7,10 +7,12 @@ using Microsoft.AspNetCore.Rewrite; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using MongoDB.Bson.Serialization; using MongoDB.Driver; using Newtonsoft.Json; using StardewModdingAPI.Toolkit.Serialisation; using StardewModdingAPI.Web.Framework; +using StardewModdingAPI.Web.Framework.Caching; using StardewModdingAPI.Web.Framework.Caching.Wiki; using StardewModdingAPI.Web.Framework.Clients.Chucklefish; using StardewModdingAPI.Web.Framework.Clients.GitHub; @@ -81,7 +83,11 @@ namespace StardewModdingAPI.Web } // init MongoDB - services.AddSingleton(serv => new MongoClient(mongoConfig.GetConnectionString()).GetDatabase(mongoConfig.Database)); + services.AddSingleton(serv => + { + BsonSerializer.RegisterSerializer(new UtcDateTimeOffsetSerializer()); + return new MongoClient(mongoConfig.GetConnectionString()).GetDatabase(mongoConfig.Database); + }); services.AddSingleton(serv => new WikiCacheRepository(serv.GetRequiredService())); // init Hangfire -- cgit From 03a082297a750dace586dc2d15d17c840d96d95f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 24 Jul 2019 18:31:43 -0400 Subject: add generic cache repository interface (#651) --- src/SMAPI.Web/Framework/Caching/ICacheRepository.cs | 13 +++++++++++++ src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMetadata.cs | 2 +- src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs | 2 +- .../Framework/Caching/Wiki/IWikiCacheRepository.cs | 9 ++------- 4 files changed, 17 insertions(+), 9 deletions(-) create mode 100644 src/SMAPI.Web/Framework/Caching/ICacheRepository.cs (limited to 'src/SMAPI.Web/Framework') diff --git a/src/SMAPI.Web/Framework/Caching/ICacheRepository.cs b/src/SMAPI.Web/Framework/Caching/ICacheRepository.cs new file mode 100644 index 00000000..5de7e731 --- /dev/null +++ b/src/SMAPI.Web/Framework/Caching/ICacheRepository.cs @@ -0,0 +1,13 @@ +using System; + +namespace StardewModdingAPI.Web.Framework.Caching +{ + /// Encapsulates logic for accessing data in the cache. + internal interface ICacheRepository + { + /// Whether cached data is stale. + /// The date when the data was updated. + /// The age in minutes before data is considered stale. + bool IsStale(DateTimeOffset lastUpdated, int staleMinutes); + } +} diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMetadata.cs b/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMetadata.cs index 4d6b4b10..6a560eb4 100644 --- a/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMetadata.cs +++ b/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMetadata.cs @@ -5,7 +5,7 @@ using MongoDB.Bson; namespace StardewModdingAPI.Web.Framework.Caching.Wiki { /// The model for cached wiki metadata. - public class CachedWikiMetadata + internal class CachedWikiMetadata { /********* ** Accessors diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs b/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs index bf1e2be2..37f55db1 100644 --- a/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs +++ b/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs @@ -7,7 +7,7 @@ using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; namespace StardewModdingAPI.Web.Framework.Caching.Wiki { /// The model for cached wiki mods. - public class CachedWikiMod + internal class CachedWikiMod { /********* ** Accessors diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs index 6031123d..b54c8a2f 100644 --- a/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs +++ b/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs @@ -5,8 +5,8 @@ using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; namespace StardewModdingAPI.Web.Framework.Caching.Wiki { - /// Encapsulates logic for accessing the mod data cache. - internal interface IWikiCacheRepository + /// Encapsulates logic for accessing the wiki data cache. + internal interface IWikiCacheRepository : ICacheRepository { /********* ** Methods @@ -15,11 +15,6 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki /// The fetched metadata. bool TryGetWikiMetadata(out CachedWikiMetadata metadata); - /// Whether cached data is stale. - /// The date when the data was updated. - /// The age in minutes before data is considered stale. - bool IsStale(DateTimeOffset lastUpdated, int staleMinutes); - /// Get the cached wiki mods. /// A filter to apply, if any. IEnumerable GetWikiMods(Expression> filter = null); -- cgit From 17c6ae7ed995344111513ca91b18ec6598ec2399 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 24 Jul 2019 18:33:26 -0400 Subject: migrate update check caching to MongoDB (#651) --- src/SMAPI.Web/Controllers/ModsApiController.cs | 68 ++++++--------- src/SMAPI.Web/Framework/Caching/Mods/CachedMod.cs | 97 ++++++++++++++++++++++ .../Framework/Caching/Mods/IModCacheRepository.cs | 26 ++++++ .../Framework/Caching/Mods/ModCacheRepository.cs | 96 +++++++++++++++++++++ .../Framework/ConfigModels/ModUpdateCheckConfig.cs | 4 - .../Framework/ModRepositories/ModInfoModel.cs | 4 +- src/SMAPI.Web/Startup.cs | 2 + src/SMAPI.Web/appsettings.json | 1 - 8 files changed, 249 insertions(+), 49 deletions(-) create mode 100644 src/SMAPI.Web/Framework/Caching/Mods/CachedMod.cs create mode 100644 src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs create mode 100644 src/SMAPI.Web/Framework/Caching/Mods/ModCacheRepository.cs (limited to 'src/SMAPI.Web/Framework') diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index a74d0d8a..195ee5bf 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -2,17 +2,17 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; using StardewModdingAPI.Toolkit.Framework.ModData; using StardewModdingAPI.Toolkit.Framework.UpdateData; +using StardewModdingAPI.Web.Framework.Caching.Mods; +using StardewModdingAPI.Web.Framework.Caching.Wiki; using StardewModdingAPI.Web.Framework.Clients.Chucklefish; using StardewModdingAPI.Web.Framework.Clients.GitHub; using StardewModdingAPI.Web.Framework.Clients.ModDrop; @@ -33,8 +33,11 @@ namespace StardewModdingAPI.Web.Controllers /// The mod repositories which provide mod metadata. private readonly IDictionary Repositories; - /// The cache in which to store mod metadata. - private readonly IMemoryCache Cache; + /// The cache in which to store wiki data. + private readonly IWikiCacheRepository WikiCache; + + /// The cache in which to store mod data. + private readonly IModCacheRepository ModCache; /// The number of minutes successful update checks should be cached before refetching them. private readonly int SuccessCacheMinutes; @@ -42,9 +45,6 @@ namespace StardewModdingAPI.Web.Controllers /// 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; - /// The internal mod metadata list. private readonly ModDatabase ModDatabase; @@ -57,22 +57,23 @@ namespace StardewModdingAPI.Web.Controllers *********/ /// Construct an instance. /// The web hosting environment. - /// The cache in which to store mod metadata. + /// The cache in which to store wiki data. + /// The cache in which to store mod metadata. /// The config settings for mod update checks. /// The Chucklefish API client. /// The GitHub API client. /// The ModDrop API client. /// The Nexus API client. - public ModsApiController(IHostingEnvironment environment, IMemoryCache cache, IOptions configProvider, IChucklefishClient chucklefish, IGitHubClient github, IModDropClient modDrop, INexusClient nexus) + public ModsApiController(IHostingEnvironment environment, IWikiCacheRepository wikiCache, IModCacheRepository modCache, IOptions configProvider, IChucklefishClient chucklefish, IGitHubClient github, IModDropClient modDrop, INexusClient nexus) { this.ModDatabase = new ModToolkit().GetModDatabase(Path.Combine(environment.WebRootPath, "SMAPI.metadata.json")); ModUpdateCheckConfig config = configProvider.Value; this.CompatibilityPageUrl = config.CompatibilityPageUrl; - this.Cache = cache; + this.WikiCache = wikiCache; + this.ModCache = modCache; this.SuccessCacheMinutes = config.SuccessCacheMinutes; this.ErrorCacheMinutes = config.ErrorCacheMinutes; - this.VersionRegex = config.SemanticVersionRegex; this.Repositories = new IModRepository[] { @@ -93,7 +94,7 @@ namespace StardewModdingAPI.Web.Controllers return new ModEntryModel[0]; // fetch wiki data - WikiModEntry[] wikiData = await this.GetWikiDataAsync(); + WikiModEntry[] wikiData = this.WikiCache.GetWikiMods().Select(p => p.GetModel()).ToArray(); IDictionary mods = new Dictionary(StringComparer.CurrentCultureIgnoreCase); foreach (ModSearchEntryModel mod in model.Mods) { @@ -218,26 +219,6 @@ namespace StardewModdingAPI.Web.Controllers return current != null && (other == null || other.IsOlderThan(current)); } - /// Get mod data from the wiki compatibility list. - private async Task GetWikiDataAsync() - { - ModToolkit toolkit = new ModToolkit(); - return await this.Cache.GetOrCreateAsync("_wiki", async entry => - { - try - { - WikiModEntry[] entries = (await toolkit.GetWikiCompatibilityListAsync()).Mods; - entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(this.SuccessCacheMinutes); - return entries; - } - catch - { - entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(this.ErrorCacheMinutes); - return new WikiModEntry[0]; - } - }); - } - /// Get the mod info for an update key. /// The namespaced update key. private async Task GetInfoForUpdateKeyAsync(string updateKey) @@ -247,24 +228,27 @@ namespace StardewModdingAPI.Web.Controllers if (!parsed.LooksValid) return new ModInfoModel().WithError(RemoteModStatus.DoesNotExist, $"The update key '{updateKey}' isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541'."); - // get matching repository - if (!this.Repositories.TryGetValue(parsed.Repository, out IModRepository repository)) - return new ModInfoModel().WithError(RemoteModStatus.DoesNotExist, $"There's no mod site with key '{parsed.Repository}'. Expected one of [{string.Join(", ", this.Repositories.Keys)}]."); - - // fetch mod info - return await this.Cache.GetOrCreateAsync($"{repository.VendorKey}:{parsed.ID}".ToLower(), async entry => + // get mod + if (!this.ModCache.TryGetMod(parsed.Repository, parsed.ID, out CachedMod mod) || this.ModCache.IsStale(mod.LastUpdated, mod.FetchStatus == RemoteModStatus.TemporaryError ? this.ErrorCacheMinutes : this.SuccessCacheMinutes)) { + // get site + if (!this.Repositories.TryGetValue(parsed.Repository, out IModRepository repository)) + return new ModInfoModel().WithError(RemoteModStatus.DoesNotExist, $"There's no mod site with key '{parsed.Repository}'. Expected one of [{string.Join(", ", this.Repositories.Keys)}]."); + + // fetch mod ModInfoModel result = await repository.GetModInfoAsync(parsed.ID); if (result.Error == null) { if (result.Version == null) result.WithError(RemoteModStatus.InvalidData, $"The update key '{updateKey}' matches a mod with no version number."); - else if (!Regex.IsMatch(result.Version, this.VersionRegex, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)) + else if (!SemanticVersion.TryParse(result.Version, out _)) result.WithError(RemoteModStatus.InvalidData, $"The update key '{updateKey}' matches a mod with invalid semantic version '{result.Version}'."); } - entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(result.Status == RemoteModStatus.TemporaryError ? this.ErrorCacheMinutes : this.SuccessCacheMinutes); - return result; - }); + + // cache mod + this.ModCache.SaveMod(repository.VendorKey, parsed.ID, result, out mod); + } + return mod.GetModel(); } /// Get update keys based on the available mod metadata, while maintaining the precedence order. diff --git a/src/SMAPI.Web/Framework/Caching/Mods/CachedMod.cs b/src/SMAPI.Web/Framework/Caching/Mods/CachedMod.cs new file mode 100644 index 00000000..fe8a7a1f --- /dev/null +++ b/src/SMAPI.Web/Framework/Caching/Mods/CachedMod.cs @@ -0,0 +1,97 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using StardewModdingAPI.Toolkit.Framework.UpdateData; +using StardewModdingAPI.Web.Framework.ModRepositories; + +namespace StardewModdingAPI.Web.Framework.Caching.Mods +{ + /// The model for cached mod data. + internal class CachedMod + { + /********* + ** Accessors + *********/ + /**** + ** Tracking + ****/ + /// The internal MongoDB ID. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Named per MongoDB conventions.")] + [BsonIgnoreIfDefault] + public ObjectId _id { get; set; } + + /// When the data was last updated. + public DateTimeOffset LastUpdated { get; set; } + + /// When the data was last requested through the web API. + public DateTimeOffset LastRequested { get; set; } + + /**** + ** Metadata + ****/ + /// The mod site on which the mod is found. + public ModRepositoryKey Site { get; set; } + + /// The mod's unique ID within the . + public string ID { get; set; } + + /// The mod availability status on the remote site. + public RemoteModStatus FetchStatus { get; set; } + + /// The error message providing more info for the , if applicable. + public string FetchError { get; set; } + + + /**** + ** Mod info + ****/ + /// The mod's display name. + public string Name { get; set; } + + /// The mod's latest version. + public string MainVersion { get; set; } + + /// The mod's latest optional or prerelease version, if newer than . + public string PreviewVersion { get; set; } + + /// The URL for the mod page. + public string Url { get; set; } + + + /********* + ** Accessors + *********/ + /// Construct an instance. + public CachedMod() { } + + /// Construct an instance. + /// The mod site on which the mod is found. + /// The mod's unique ID within the . + /// The mod data. + public CachedMod(ModRepositoryKey site, string id, ModInfoModel mod) + { + // tracking + this.LastUpdated = DateTimeOffset.UtcNow; + this.LastRequested = DateTimeOffset.UtcNow; + + // metadata + this.Site = site; + this.ID = id; + this.FetchStatus = mod.Status; + this.FetchError = mod.Error; + + // mod info + this.Name = mod.Name; + this.MainVersion = mod.Version; + this.PreviewVersion = mod.PreviewVersion; + this.Url = mod.Url; + } + + /// Get the API model for the cached data. + public ModInfoModel GetModel() + { + return new ModInfoModel(name: this.Name, version: this.MainVersion, previewVersion: this.PreviewVersion, url: this.Url).WithError(this.FetchStatus, this.FetchError); + } + } +} diff --git a/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs new file mode 100644 index 00000000..23929d1d --- /dev/null +++ b/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs @@ -0,0 +1,26 @@ +using StardewModdingAPI.Toolkit.Framework.UpdateData; +using StardewModdingAPI.Web.Framework.ModRepositories; + +namespace StardewModdingAPI.Web.Framework.Caching.Mods +{ + /// Encapsulates logic for accessing the mod data cache. + internal interface IModCacheRepository : ICacheRepository + { + /********* + ** Methods + *********/ + /// Get the cached mod data. + /// The mod site to search. + /// The mod's unique ID within the . + /// The fetched mod. + /// Whether to update the mod's 'last requested' date. + bool TryGetMod(ModRepositoryKey site, string id, out CachedMod mod, bool markRequested = true); + + /// Save data fetched for a mod. + /// The mod site on which the mod is found. + /// The mod's unique ID within the . + /// The mod data. + /// The stored mod record. + void SaveMod(ModRepositoryKey site, string id, ModInfoModel mod, out CachedMod cachedMod); + } +} diff --git a/src/SMAPI.Web/Framework/Caching/Mods/ModCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/Mods/ModCacheRepository.cs new file mode 100644 index 00000000..d8ad7d21 --- /dev/null +++ b/src/SMAPI.Web/Framework/Caching/Mods/ModCacheRepository.cs @@ -0,0 +1,96 @@ +using System; +using MongoDB.Driver; +using StardewModdingAPI.Toolkit.Framework.UpdateData; +using StardewModdingAPI.Web.Framework.ModRepositories; + +namespace StardewModdingAPI.Web.Framework.Caching.Mods +{ + /// Encapsulates logic for accessing the mod data cache. + internal class ModCacheRepository : BaseCacheRepository, IModCacheRepository + { + /********* + ** Fields + *********/ + /// The collection for cached mod data. + private readonly IMongoCollection Mods; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The authenticated MongoDB database. + public ModCacheRepository(IMongoDatabase database) + { + // get collections + this.Mods = database.GetCollection("mods"); + + // add indexes if needed + this.Mods.Indexes.CreateOne(new CreateIndexModel(Builders.IndexKeys.Ascending(p => p.ID).Ascending(p => p.Site))); + } + + /********* + ** Public methods + *********/ + /// Get the cached mod data. + /// The mod site to search. + /// The mod's unique ID within the . + /// The fetched mod. + /// Whether to update the mod's 'last requested' date. + public bool TryGetMod(ModRepositoryKey site, string id, out CachedMod mod, bool markRequested = true) + { + // get mod + id = this.NormaliseId(id); + mod = this.Mods.Find(entry => entry.ID == id && entry.Site == site).FirstOrDefault(); + if (mod == null) + return false; + + // bump 'last requested' + if (markRequested) + { + mod.LastRequested = DateTimeOffset.UtcNow; + mod = this.SaveMod(mod); + } + + return true; + } + + /// Save data fetched for a mod. + /// The mod site on which the mod is found. + /// The mod's unique ID within the . + /// The mod data. + /// The stored mod record. + public void SaveMod(ModRepositoryKey site, string id, ModInfoModel mod, out CachedMod cachedMod) + { + id = this.NormaliseId(id); + + cachedMod = this.SaveMod(new CachedMod(site, id, mod)); + } + + + /********* + ** Private methods + *********/ + /// Save data fetched for a mod. + /// The mod data. + public CachedMod SaveMod(CachedMod mod) + { + string id = this.NormaliseId(mod.ID); + + this.Mods.ReplaceOne( + entry => entry.ID == id && entry.Site == mod.Site, + mod, + new UpdateOptions { IsUpsert = true } + ); + + return mod; + } + + /// Normalise a mod ID for case-insensitive search. + /// The mod ID. + public string NormaliseId(string id) + { + return id.Trim().ToLower(); + } + } +} diff --git a/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs index bde566c0..ab935bb3 100644 --- a/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs +++ b/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs @@ -12,10 +12,6 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels /// 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. - public string SemanticVersionRegex { get; set; } - /// The web URL for the wiki compatibility list. public string CompatibilityPageUrl { get; set; } } diff --git a/src/SMAPI.Web/Framework/ModRepositories/ModInfoModel.cs b/src/SMAPI.Web/Framework/ModRepositories/ModInfoModel.cs index 16885bfd..15e6c213 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/ModInfoModel.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/ModInfoModel.cs @@ -18,7 +18,7 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories /// The mod's web URL. public string Url { get; set; } - /// The mod availability status. + /// The mod availability status on the remote site. public RemoteModStatus Status { get; set; } = RemoteModStatus.Ok; /// The error message indicating why the mod is invalid (if applicable). @@ -45,7 +45,7 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories } /// Set a mod error. - /// The mod availability status. + /// The mod availability status on the remote site. /// The error message indicating why the mod is invalid (if applicable). public ModInfoModel WithError(RemoteModStatus status, string error) { diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index caa7b056..fd229b5e 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -13,6 +13,7 @@ using Newtonsoft.Json; using StardewModdingAPI.Toolkit.Serialisation; using StardewModdingAPI.Web.Framework; using StardewModdingAPI.Web.Framework.Caching; +using StardewModdingAPI.Web.Framework.Caching.Mods; using StardewModdingAPI.Web.Framework.Caching.Wiki; using StardewModdingAPI.Web.Framework.Clients.Chucklefish; using StardewModdingAPI.Web.Framework.Clients.GitHub; @@ -88,6 +89,7 @@ namespace StardewModdingAPI.Web BsonSerializer.RegisterSerializer(new UtcDateTimeOffsetSerializer()); return new MongoClient(mongoConfig.GetConnectionString()).GetDatabase(mongoConfig.Database); }); + services.AddSingleton(serv => new ModCacheRepository(serv.GetRequiredService())); services.AddSingleton(serv => new WikiCacheRepository(serv.GetRequiredService())); // init Hangfire diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json index ea7e9cd2..77d13924 100644 --- a/src/SMAPI.Web/appsettings.json +++ b/src/SMAPI.Web/appsettings.json @@ -66,7 +66,6 @@ "ModUpdateCheck": { "SuccessCacheMinutes": 60, "ErrorCacheMinutes": 5, - "SemanticVersionRegex": "^(?>(?0|[1-9]\\d*))\\.(?>(?0|[1-9]\\d*))(?>(?:\\.(?0|[1-9]\\d*))?)(?:-(?(?>[a-z0-9]+[\\-\\.]?)+))?$", "CompatibilityPageUrl": "https://mods.smapi.io" } } -- cgit From edc00ddaab46a2a2d0ba07591a6206159421ef41 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 24 Jul 2019 18:34:28 -0400 Subject: remove cached mod data not requested within 48 hours (#651) --- src/SMAPI.Web/BackgroundService.cs | 22 ++++++++++++++++++---- .../Framework/Caching/Mods/IModCacheRepository.cs | 5 +++++ .../Framework/Caching/Mods/ModCacheRepository.cs | 8 ++++++++ 3 files changed, 31 insertions(+), 4 deletions(-) (limited to 'src/SMAPI.Web/Framework') diff --git a/src/SMAPI.Web/BackgroundService.cs b/src/SMAPI.Web/BackgroundService.cs index 2ccfd5f7..2dd3b921 100644 --- a/src/SMAPI.Web/BackgroundService.cs +++ b/src/SMAPI.Web/BackgroundService.cs @@ -5,6 +5,7 @@ using Hangfire; using Microsoft.Extensions.Hosting; using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; +using StardewModdingAPI.Web.Framework.Caching.Mods; using StardewModdingAPI.Web.Framework.Caching.Wiki; namespace StardewModdingAPI.Web @@ -19,9 +20,12 @@ namespace StardewModdingAPI.Web /// The background task server. private static BackgroundJobServer JobServer; - /// The cache in which to store mod metadata. + /// The cache in which to store wiki metadata. private static IWikiCacheRepository WikiCache; + /// The cache in which to store mod data. + private static IModCacheRepository ModCache; + /********* ** Public methods @@ -30,10 +34,12 @@ namespace StardewModdingAPI.Web ** Hosted service ****/ /// Construct an instance. - /// The cache in which to store mod metadata. - public BackgroundService(IWikiCacheRepository wikiCache) + /// The cache in which to store wiki metadata. + /// The cache in which to store mod data. + public BackgroundService(IWikiCacheRepository wikiCache, IModCacheRepository modCache) { BackgroundService.WikiCache = wikiCache; + BackgroundService.ModCache = modCache; } /// Start the service. @@ -44,9 +50,11 @@ namespace StardewModdingAPI.Web // set startup tasks BackgroundJob.Enqueue(() => BackgroundService.UpdateWikiAsync()); + BackgroundJob.Enqueue(() => BackgroundService.RemoveStaleMods()); // set recurring tasks - RecurringJob.AddOrUpdate(() => BackgroundService.UpdateWikiAsync(), "*/10 * * * *"); + RecurringJob.AddOrUpdate(() => BackgroundService.UpdateWikiAsync(), "*/10 * * * *"); // every 10 minutes + RecurringJob.AddOrUpdate(() => BackgroundService.RemoveStaleMods(), "0 * * * *"); // hourly return Task.CompletedTask; } @@ -75,6 +83,12 @@ namespace StardewModdingAPI.Web BackgroundService.WikiCache.SaveWikiData(wikiCompatList.StableVersion, wikiCompatList.BetaVersion, wikiCompatList.Mods, out _, out _); } + /// Remove mods which haven't been requested in over 48 hours. + public static async Task RemoveStaleMods() + { + BackgroundService.ModCache.RemoveStaleMods(TimeSpan.FromHours(48)); + } + /********* ** Private method diff --git a/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs index 23929d1d..bcec8b36 100644 --- a/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs +++ b/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs @@ -1,3 +1,4 @@ +using System; using StardewModdingAPI.Toolkit.Framework.UpdateData; using StardewModdingAPI.Web.Framework.ModRepositories; @@ -22,5 +23,9 @@ namespace StardewModdingAPI.Web.Framework.Caching.Mods /// The mod data. /// The stored mod record. void SaveMod(ModRepositoryKey site, string id, ModInfoModel mod, out CachedMod cachedMod); + + /// Delete data for mods which haven't been requested within a given time limit. + /// The minimum age for which to remove mods. + void RemoveStaleMods(TimeSpan age); } } diff --git a/src/SMAPI.Web/Framework/Caching/Mods/ModCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/Mods/ModCacheRepository.cs index d8ad7d21..4258cc85 100644 --- a/src/SMAPI.Web/Framework/Caching/Mods/ModCacheRepository.cs +++ b/src/SMAPI.Web/Framework/Caching/Mods/ModCacheRepository.cs @@ -67,6 +67,14 @@ namespace StardewModdingAPI.Web.Framework.Caching.Mods cachedMod = this.SaveMod(new CachedMod(site, id, mod)); } + /// Delete data for mods which haven't been requested within a given time limit. + /// The minimum age for which to remove mods. + public void RemoveStaleMods(TimeSpan age) + { + DateTimeOffset minDate = DateTimeOffset.UtcNow.Subtract(age); + var result = this.Mods.DeleteMany(p => p.LastRequested < minDate); + } + /********* ** Private methods -- cgit From 08d83aa039dd57efcc6ab50595fe5c0ea1003527 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 24 Jul 2019 21:28:33 -0400 Subject: treat hidden/unpublished Nexus mods as not found (#651) --- src/SMAPI.Web/Framework/Clients/Nexus/NexusMod.cs | 4 ++++ .../Framework/Clients/Nexus/NexusModStatus.cs | 18 ++++++++++++++++++ .../Framework/Clients/Nexus/NexusWebScrapeClient.cs | 8 +++++++- .../Framework/ModRepositories/NexusRepository.cs | 8 +++++++- 4 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 src/SMAPI.Web/Framework/Clients/Nexus/NexusModStatus.cs (limited to 'src/SMAPI.Web/Framework') diff --git a/src/SMAPI.Web/Framework/Clients/Nexus/NexusMod.cs b/src/SMAPI.Web/Framework/Clients/Nexus/NexusMod.cs index f4909155..0f1b29d5 100644 --- a/src/SMAPI.Web/Framework/Clients/Nexus/NexusMod.cs +++ b/src/SMAPI.Web/Framework/Clients/Nexus/NexusMod.cs @@ -21,6 +21,10 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus [JsonProperty("mod_page_uri")] public string Url { get; set; } + /// The mod's publication status. + [JsonIgnore] + public NexusModStatus Status { get; set; } = NexusModStatus.Ok; + /// A user-friendly error which indicates why fetching the mod info failed (if applicable). [JsonIgnore] public string Error { get; set; } diff --git a/src/SMAPI.Web/Framework/Clients/Nexus/NexusModStatus.cs b/src/SMAPI.Web/Framework/Clients/Nexus/NexusModStatus.cs new file mode 100644 index 00000000..c5723093 --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/Nexus/NexusModStatus.cs @@ -0,0 +1,18 @@ +namespace StardewModdingAPI.Web.Framework.Clients.Nexus +{ + /// The status of a Nexus mod. + internal enum NexusModStatus + { + /// The mod is published and valid. + Ok, + + /// The mod is hidden by the author. + Hidden, + + /// The mod hasn't been published yet. + NotPublished, + + /// The Nexus API returned an unhandled error. + Other + } +} diff --git a/src/SMAPI.Web/Framework/Clients/Nexus/NexusWebScrapeClient.cs b/src/SMAPI.Web/Framework/Clients/Nexus/NexusWebScrapeClient.cs index e83a6041..061de5de 100644 --- a/src/SMAPI.Web/Framework/Clients/Nexus/NexusWebScrapeClient.cs +++ b/src/SMAPI.Web/Framework/Clients/Nexus/NexusWebScrapeClient.cs @@ -74,8 +74,14 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus case "not found": return null; + case "hidden mod": + return new NexusMod { Error = $"Nexus error: {errorCode} ({errorText}).", Status = NexusModStatus.Hidden }; + + case "not published": + return new NexusMod { Error = $"Nexus error: {errorCode} ({errorText}).", Status = NexusModStatus.NotPublished }; + default: - return new NexusMod { Error = $"Nexus error: {errorCode} ({errorText})." }; + return new NexusMod { Error = $"Nexus error: {errorCode} ({errorText}).", Status = NexusModStatus.Other }; } } diff --git a/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs index 9679f93e..a4ae61eb 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs @@ -41,7 +41,13 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories if (mod == null) return new ModInfoModel().WithError(RemoteModStatus.DoesNotExist, "Found no Nexus mod with this ID."); if (mod.Error != null) - return new ModInfoModel().WithError(RemoteModStatus.TemporaryError, mod.Error); + { + RemoteModStatus remoteStatus = mod.Status == NexusModStatus.Hidden || mod.Status == NexusModStatus.NotPublished + ? RemoteModStatus.DoesNotExist + : RemoteModStatus.TemporaryError; + return new ModInfoModel().WithError(remoteStatus, mod.Error); + } + return new ModInfoModel(name: mod.Name, version: this.NormaliseVersion(mod.Version), previewVersion: mod.LatestFileVersion?.ToString(), url: mod.Url); } catch (Exception ex) -- cgit From 890c6b3ea7c4aedf4a9130aceff8d80c78bd6e0f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 24 Jul 2019 22:08:15 -0400 Subject: rename Nexus API client for upcoming API usage (#651) --- .../Framework/Clients/Nexus/NexusClient.cs | 152 +++++++++++++++++++++ .../Clients/Nexus/NexusWebScrapeClient.cs | 152 --------------------- src/SMAPI.Web/Startup.cs | 2 +- 3 files changed, 153 insertions(+), 153 deletions(-) create mode 100644 src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs delete mode 100644 src/SMAPI.Web/Framework/Clients/Nexus/NexusWebScrapeClient.cs (limited to 'src/SMAPI.Web/Framework') diff --git a/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs b/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs new file mode 100644 index 00000000..87393367 --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using HtmlAgilityPack; +using Pathoschild.Http.Client; +using StardewModdingAPI.Toolkit; + +namespace StardewModdingAPI.Web.Framework.Clients.Nexus +{ + /// An HTTP client for fetching mod metadata from the Nexus website. + internal class NexusClient : INexusClient + { + /********* + ** Fields + *********/ + /// The URL for a Nexus mod page for the user, excluding the base URL, where {0} is the mod ID. + private readonly string ModUrlFormat; + + /// The URL for a Nexus mod page to scrape for versions, excluding the base URL, where {0} is the mod ID. + public string ModScrapeUrlFormat { get; set; } + + /// The underlying HTTP client. + private readonly IClient Client; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The user agent for the Nexus Mods API client. + /// The base URL for the Nexus Mods site. + /// The URL for a Nexus Mods mod page for the user, excluding the , where {0} is the mod ID. + /// The URL for a Nexus mod page to scrape for versions, excluding the base URL, where {0} is the mod ID. + public NexusClient(string userAgent, string baseUrl, string modUrlFormat, string modScrapeUrlFormat) + { + this.ModUrlFormat = modUrlFormat; + this.ModScrapeUrlFormat = modScrapeUrlFormat; + this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent); + } + + /// Get metadata about a mod. + /// The Nexus mod ID. + /// Returns the mod info if found, else null. + public async Task GetModAsync(uint id) + { + // fetch HTML + string html; + try + { + html = await this.Client + .GetAsync(string.Format(this.ModScrapeUrlFormat, id)) + .AsString(); + } + catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound) + { + return null; + } + + // parse HTML + var doc = new HtmlDocument(); + doc.LoadHtml(html); + + // handle Nexus error message + HtmlNode node = doc.DocumentNode.SelectSingleNode("//div[contains(@class, 'site-notice')][contains(@class, 'warning')]"); + if (node != null) + { + string[] errorParts = node.InnerText.Trim().Split(new[] { '\n' }, 2, System.StringSplitOptions.RemoveEmptyEntries); + string errorCode = errorParts[0]; + string errorText = errorParts.Length > 1 ? errorParts[1] : null; + switch (errorCode.Trim().ToLower()) + { + case "not found": + return null; + + case "hidden mod": + return new NexusMod { Error = $"Nexus error: {errorCode} ({errorText}).", Status = NexusModStatus.Hidden }; + + case "not published": + return new NexusMod { Error = $"Nexus error: {errorCode} ({errorText}).", Status = NexusModStatus.NotPublished }; + + default: + return new NexusMod { Error = $"Nexus error: {errorCode} ({errorText}).", Status = NexusModStatus.Other }; + } + } + + // extract mod info + string url = this.GetModUrl(id); + string name = doc.DocumentNode.SelectSingleNode("//h1")?.InnerText.Trim(); + string version = doc.DocumentNode.SelectSingleNode("//ul[contains(@class, 'stats')]//li[@class='stat-version']//div[@class='stat']")?.InnerText.Trim(); + SemanticVersion.TryParse(version, out ISemanticVersion parsedVersion); + + // extract file versions + List rawVersions = new List(); + foreach (var fileSection in doc.DocumentNode.SelectNodes("//div[contains(@class, 'files-tabs')]")) + { + string sectionName = fileSection.Descendants("h2").First().InnerText; + if (sectionName != "Main files" && sectionName != "Optional files") + continue; + + rawVersions.AddRange( + from statBox in fileSection.Descendants().Where(p => p.HasClass("stat-version")) + from versionStat in statBox.Descendants().Where(p => p.HasClass("stat")) + select versionStat.InnerText.Trim() + ); + } + + // choose latest file version + ISemanticVersion latestFileVersion = null; + foreach (string rawVersion in rawVersions) + { + if (!SemanticVersion.TryParse(rawVersion, out ISemanticVersion cur)) + continue; + if (parsedVersion != null && !cur.IsNewerThan(parsedVersion)) + continue; + if (latestFileVersion != null && !cur.IsNewerThan(latestFileVersion)) + continue; + + latestFileVersion = cur; + } + + // yield info + return new NexusMod + { + Name = name, + Version = parsedVersion?.ToString() ?? version, + LatestFileVersion = latestFileVersion, + Url = url + }; + } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public void Dispose() + { + this.Client?.Dispose(); + } + + + /********* + ** Private methods + *********/ + /// Get the full mod page URL for a given ID. + /// The mod ID. + private string GetModUrl(uint id) + { + UriBuilder builder = new UriBuilder(this.Client.BaseClient.BaseAddress); + builder.Path += string.Format(this.ModUrlFormat, id); + return builder.Uri.ToString(); + } + } +} diff --git a/src/SMAPI.Web/Framework/Clients/Nexus/NexusWebScrapeClient.cs b/src/SMAPI.Web/Framework/Clients/Nexus/NexusWebScrapeClient.cs deleted file mode 100644 index 061de5de..00000000 --- a/src/SMAPI.Web/Framework/Clients/Nexus/NexusWebScrapeClient.cs +++ /dev/null @@ -1,152 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using HtmlAgilityPack; -using Pathoschild.Http.Client; -using StardewModdingAPI.Toolkit; - -namespace StardewModdingAPI.Web.Framework.Clients.Nexus -{ - /// An HTTP client for fetching mod metadata from the Nexus website. - internal class NexusWebScrapeClient : INexusClient - { - /********* - ** Fields - *********/ - /// The URL for a Nexus mod page for the user, excluding the base URL, where {0} is the mod ID. - private readonly string ModUrlFormat; - - /// The URL for a Nexus mod page to scrape for versions, excluding the base URL, where {0} is the mod ID. - public string ModScrapeUrlFormat { get; set; } - - /// The underlying HTTP client. - private readonly IClient Client; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The user agent for the Nexus Mods API client. - /// The base URL for the Nexus Mods site. - /// The URL for a Nexus Mods mod page for the user, excluding the , where {0} is the mod ID. - /// The URL for a Nexus mod page to scrape for versions, excluding the base URL, where {0} is the mod ID. - public NexusWebScrapeClient(string userAgent, string baseUrl, string modUrlFormat, string modScrapeUrlFormat) - { - this.ModUrlFormat = modUrlFormat; - this.ModScrapeUrlFormat = modScrapeUrlFormat; - this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent); - } - - /// Get metadata about a mod. - /// The Nexus mod ID. - /// Returns the mod info if found, else null. - public async Task GetModAsync(uint id) - { - // fetch HTML - string html; - try - { - html = await this.Client - .GetAsync(string.Format(this.ModScrapeUrlFormat, id)) - .AsString(); - } - catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound) - { - return null; - } - - // parse HTML - var doc = new HtmlDocument(); - doc.LoadHtml(html); - - // handle Nexus error message - HtmlNode node = doc.DocumentNode.SelectSingleNode("//div[contains(@class, 'site-notice')][contains(@class, 'warning')]"); - if (node != null) - { - string[] errorParts = node.InnerText.Trim().Split(new[] { '\n' }, 2, System.StringSplitOptions.RemoveEmptyEntries); - string errorCode = errorParts[0]; - string errorText = errorParts.Length > 1 ? errorParts[1] : null; - switch (errorCode.Trim().ToLower()) - { - case "not found": - return null; - - case "hidden mod": - return new NexusMod { Error = $"Nexus error: {errorCode} ({errorText}).", Status = NexusModStatus.Hidden }; - - case "not published": - return new NexusMod { Error = $"Nexus error: {errorCode} ({errorText}).", Status = NexusModStatus.NotPublished }; - - default: - return new NexusMod { Error = $"Nexus error: {errorCode} ({errorText}).", Status = NexusModStatus.Other }; - } - } - - // extract mod info - string url = this.GetModUrl(id); - string name = doc.DocumentNode.SelectSingleNode("//h1")?.InnerText.Trim(); - string version = doc.DocumentNode.SelectSingleNode("//ul[contains(@class, 'stats')]//li[@class='stat-version']//div[@class='stat']")?.InnerText.Trim(); - SemanticVersion.TryParse(version, out ISemanticVersion parsedVersion); - - // extract file versions - List rawVersions = new List(); - foreach (var fileSection in doc.DocumentNode.SelectNodes("//div[contains(@class, 'files-tabs')]")) - { - string sectionName = fileSection.Descendants("h2").First().InnerText; - if (sectionName != "Main files" && sectionName != "Optional files") - continue; - - rawVersions.AddRange( - from statBox in fileSection.Descendants().Where(p => p.HasClass("stat-version")) - from versionStat in statBox.Descendants().Where(p => p.HasClass("stat")) - select versionStat.InnerText.Trim() - ); - } - - // choose latest file version - ISemanticVersion latestFileVersion = null; - foreach (string rawVersion in rawVersions) - { - if (!SemanticVersion.TryParse(rawVersion, out ISemanticVersion cur)) - continue; - if (parsedVersion != null && !cur.IsNewerThan(parsedVersion)) - continue; - if (latestFileVersion != null && !cur.IsNewerThan(latestFileVersion)) - continue; - - latestFileVersion = cur; - } - - // yield info - return new NexusMod - { - Name = name, - Version = parsedVersion?.ToString() ?? version, - LatestFileVersion = latestFileVersion, - Url = url - }; - } - - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - public void Dispose() - { - this.Client?.Dispose(); - } - - - /********* - ** Private methods - *********/ - /// Get the full mod page URL for a given ID. - /// The mod ID. - private string GetModUrl(uint id) - { - UriBuilder builder = new UriBuilder(this.Client.BaseClient.BaseAddress); - builder.Path += string.Format(this.ModUrlFormat, id); - return builder.Uri.ToString(); - } - } -} diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index fd229b5e..66e1f00d 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -135,7 +135,7 @@ namespace StardewModdingAPI.Web modUrlFormat: api.ModDropModPageUrl )); - services.AddSingleton(new NexusWebScrapeClient( + services.AddSingleton(new NexusClient( userAgent: userAgent, baseUrl: api.NexusBaseUrl, modUrlFormat: api.NexusModUrlFormat, -- cgit From 95f261b1f30d8c5ad6c179cd75a220dcca3c6395 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 24 Jul 2019 22:11:51 -0400 Subject: fetch mod info from Nexus API if the web page is hidden due to adult content (#651) --- docs/release-notes.md | 1 + .../Framework/Clients/Nexus/NexusClient.cs | 133 ++++++++++++++++----- .../Framework/Clients/Nexus/NexusModStatus.cs | 3 + .../Framework/ConfigModels/ApiClientsConfig.cs | 3 + src/SMAPI.Web/SMAPI.Web.csproj | 1 + src/SMAPI.Web/Startup.cs | 10 +- src/SMAPI.Web/appsettings.Development.json | 2 + src/SMAPI.Web/appsettings.json | 1 + 8 files changed, 120 insertions(+), 34 deletions(-) (limited to 'src/SMAPI.Web/Framework') diff --git a/docs/release-notes.md b/docs/release-notes.md index 4b380564..2b7d1545 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -24,6 +24,7 @@ These changes have not been released yet. * Fixed map reloads resetting tilesheet seasons. * Fixed map reloads not updating door warps. * Fixed outdoor tilesheets being seasonalised when added to an indoor location. + * Fixed update checks failing for Nexus mods marked as adult content. * For the mod compatibility list: * Now loads faster (since data is fetched in a background service). diff --git a/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs b/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs index 87393367..753d3b4f 100644 --- a/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs +++ b/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs @@ -4,8 +4,10 @@ using System.Linq; using System.Net; using System.Threading.Tasks; using HtmlAgilityPack; +using Pathoschild.FluentNexus.Models; using Pathoschild.Http.Client; using StardewModdingAPI.Toolkit; +using FluentNexusClient = Pathoschild.FluentNexus.NexusClient; namespace StardewModdingAPI.Web.Framework.Clients.Nexus { @@ -16,41 +18,73 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus ** Fields *********/ /// The URL for a Nexus mod page for the user, excluding the base URL, where {0} is the mod ID. - private readonly string ModUrlFormat; + private readonly string WebModUrlFormat; /// The URL for a Nexus mod page to scrape for versions, excluding the base URL, where {0} is the mod ID. - public string ModScrapeUrlFormat { get; set; } + public string WebModScrapeUrlFormat { get; set; } - /// The underlying HTTP client. - private readonly IClient Client; + /// The underlying HTTP client for the Nexus Mods website. + private readonly IClient WebClient; + + /// The underlying HTTP client for the Nexus API. + private readonly FluentNexusClient ApiClient; /********* ** Public methods *********/ /// Construct an instance. - /// The user agent for the Nexus Mods API client. - /// The base URL for the Nexus Mods site. - /// The URL for a Nexus Mods mod page for the user, excluding the , where {0} is the mod ID. - /// The URL for a Nexus mod page to scrape for versions, excluding the base URL, where {0} is the mod ID. - public NexusClient(string userAgent, string baseUrl, string modUrlFormat, string modScrapeUrlFormat) + /// The user agent for the Nexus Mods web client. + /// The base URL for the Nexus Mods site. + /// The URL for a Nexus Mods mod page for the user, excluding the , where {0} is the mod ID. + /// The URL for a Nexus mod page to scrape for versions, excluding the base URL, where {0} is the mod ID. + /// The app version to show in API user agents. + /// The Nexus API authentication key. + public NexusClient(string webUserAgent, string webBaseUrl, string webModUrlFormat, string webModScrapeUrlFormat, string apiAppVersion, string apiKey) { - this.ModUrlFormat = modUrlFormat; - this.ModScrapeUrlFormat = modScrapeUrlFormat; - this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent); + this.WebModUrlFormat = webModUrlFormat; + this.WebModScrapeUrlFormat = webModScrapeUrlFormat; + this.WebClient = new FluentClient(webBaseUrl).SetUserAgent(webUserAgent); + this.ApiClient = new FluentNexusClient(apiKey, "SMAPI", apiAppVersion); } /// Get metadata about a mod. /// The Nexus mod ID. /// Returns the mod info if found, else null. public async Task GetModAsync(uint id) + { + // Fetch from the Nexus website when possible, since it has no rate limits. Mods with + // adult content are hidden for anonymous users, so fall back to the API in that case. + // Note that the API has very restrictive rate limits which means we can't just use it + // for all cases. + NexusMod mod = await this.GetModFromWebsiteAsync(id); + if (mod?.Status == NexusModStatus.AdultContentForbidden) + mod = await this.GetModFromApiAsync(id); + + return mod; + } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public void Dispose() + { + this.WebClient?.Dispose(); + } + + + /********* + ** Private methods + *********/ + /// Get metadata about a mod by scraping the Nexus website. + /// The Nexus mod ID. + /// Returns the mod info if found, else null. + private async Task GetModFromWebsiteAsync(uint id) { // fetch HTML string html; try { - html = await this.Client - .GetAsync(string.Format(this.ModScrapeUrlFormat, id)) + html = await this.WebClient + .GetAsync(string.Format(this.WebModScrapeUrlFormat, id)) .AsString(); } catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound) @@ -74,14 +108,8 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus case "not found": return null; - case "hidden mod": - return new NexusMod { Error = $"Nexus error: {errorCode} ({errorText}).", Status = NexusModStatus.Hidden }; - - case "not published": - return new NexusMod { Error = $"Nexus error: {errorCode} ({errorText}).", Status = NexusModStatus.NotPublished }; - default: - return new NexusMod { Error = $"Nexus error: {errorCode} ({errorText}).", Status = NexusModStatus.Other }; + return new NexusMod { Error = $"Nexus error: {errorCode} ({errorText}).", Status = this.GetWebStatus(errorCode) }; } } @@ -130,23 +158,68 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus }; } - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - public void Dispose() + /// Get metadata about a mod from the Nexus API. + /// The Nexus mod ID. + /// Returns the mod info if found, else null. + private async Task GetModFromApiAsync(uint id) { - this.Client?.Dispose(); - } + // fetch mod + Mod mod = await this.ApiClient.Mods.GetMod("stardewvalley", (int)id); + ModFileList files = await this.ApiClient.ModFiles.GetModFiles("stardewvalley", (int)id, FileCategory.Main, FileCategory.Optional); + + // get versions + if (!SemanticVersion.TryParse(mod.Version, out ISemanticVersion mainVersion)) + mainVersion = null; + ISemanticVersion latestFileVersion = null; + foreach (string rawVersion in files.Files.Select(p => p.FileVersion)) + { + if (!SemanticVersion.TryParse(rawVersion, out ISemanticVersion cur)) + continue; + if (mainVersion != null && !cur.IsNewerThan(mainVersion)) + continue; + if (latestFileVersion != null && !cur.IsNewerThan(latestFileVersion)) + continue; + latestFileVersion = cur; + } + + // yield info + return new NexusMod + { + Name = mod.Name, + Version = SemanticVersion.TryParse(mod.Version, out ISemanticVersion version) ? version?.ToString() : mod.Version, + LatestFileVersion = latestFileVersion, + Url = this.GetModUrl(id) + }; + } - /********* - ** Private methods - *********/ /// Get the full mod page URL for a given ID. /// The mod ID. private string GetModUrl(uint id) { - UriBuilder builder = new UriBuilder(this.Client.BaseClient.BaseAddress); - builder.Path += string.Format(this.ModUrlFormat, id); + UriBuilder builder = new UriBuilder(this.WebClient.BaseClient.BaseAddress); + builder.Path += string.Format(this.WebModUrlFormat, id); return builder.Uri.ToString(); } + + /// Get the mod status for a web error code. + /// The Nexus error code. + private NexusModStatus GetWebStatus(string errorCode) + { + switch (errorCode.Trim().ToLower()) + { + case "adult content": + return NexusModStatus.AdultContentForbidden; + + case "hidden mod": + return NexusModStatus.Hidden; + + case "not published": + return NexusModStatus.NotPublished; + + default: + return NexusModStatus.Other; + } + } } } diff --git a/src/SMAPI.Web/Framework/Clients/Nexus/NexusModStatus.cs b/src/SMAPI.Web/Framework/Clients/Nexus/NexusModStatus.cs index c5723093..9ef314cd 100644 --- a/src/SMAPI.Web/Framework/Clients/Nexus/NexusModStatus.cs +++ b/src/SMAPI.Web/Framework/Clients/Nexus/NexusModStatus.cs @@ -12,6 +12,9 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus /// The mod hasn't been published yet. NotPublished, + /// The mod contains adult content which is hidden for anonymous web users. + AdultContentForbidden, + /// The Nexus API returned an unhandled error. Other } diff --git a/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs index c27cadab..d2e9a2fe 100644 --- a/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs +++ b/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs @@ -65,6 +65,9 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels /// The URL for a Nexus mod page to scrape for versions, excluding the , where {0} is the mod ID. public string NexusModScrapeUrlFormat { get; set; } + /// The Nexus API authentication key. + public string NexusApiKey { get; set; } + /**** ** Pastebin ****/ diff --git a/src/SMAPI.Web/SMAPI.Web.csproj b/src/SMAPI.Web/SMAPI.Web.csproj index f4e99e0c..26b29fce 100644 --- a/src/SMAPI.Web/SMAPI.Web.csproj +++ b/src/SMAPI.Web/SMAPI.Web.csproj @@ -26,6 +26,7 @@ + diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index 66e1f00d..4d23fe65 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -136,10 +136,12 @@ namespace StardewModdingAPI.Web )); services.AddSingleton(new NexusClient( - userAgent: userAgent, - baseUrl: api.NexusBaseUrl, - modUrlFormat: api.NexusModUrlFormat, - modScrapeUrlFormat: api.NexusModScrapeUrlFormat + webUserAgent: userAgent, + webBaseUrl: api.NexusBaseUrl, + webModUrlFormat: api.NexusModUrlFormat, + webModScrapeUrlFormat: api.NexusModScrapeUrlFormat, + apiAppVersion: version, + apiKey: api.NexusApiKey )); services.AddSingleton(new PastebinClient( diff --git a/src/SMAPI.Web/appsettings.Development.json b/src/SMAPI.Web/appsettings.Development.json index 5856b96c..e6b4a1b1 100644 --- a/src/SMAPI.Web/appsettings.Development.json +++ b/src/SMAPI.Web/appsettings.Development.json @@ -20,6 +20,8 @@ "GitHubUsername": null, "GitHubPassword": null, + "NexusApiKey": null, + "PastebinUserKey": null, "PastebinDevKey": null }, diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json index 77d13924..3ea37dea 100644 --- a/src/SMAPI.Web/appsettings.json +++ b/src/SMAPI.Web/appsettings.json @@ -39,6 +39,7 @@ "ModDropApiUrl": "https://www.moddrop.com/api/mods/data", "ModDropModPageUrl": "https://www.moddrop.com/sdv/mod/{0}", + "NexusApiKey": null, // see top note "NexusBaseUrl": "https://www.nexusmods.com/stardewvalley/", "NexusModUrlFormat": "mods/{0}", "NexusModScrapeUrlFormat": "mods/{0}?tab=files", -- cgit From 1d085df5b796e02b3e9e6874bd4e5684e840cb92 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 29 Jul 2019 16:43:25 -0400 Subject: track license info for mod GitHub repos (#651) --- docs/release-notes.md | 1 + .../Framework/Clients/Wiki/WikiModEntry.cs | 1 - .../Framework/UpdateData/UpdateKey.cs | 32 ++++++++++++- src/SMAPI.Web/BackgroundService.cs | 7 +-- src/SMAPI.Web/Controllers/ModsApiController.cs | 54 ++++++++++++++-------- src/SMAPI.Web/Framework/Caching/Mods/CachedMod.cs | 12 ++++- .../Framework/Clients/GitHub/GitHubClient.cs | 39 +++++++++------- .../Framework/Clients/GitHub/GitLicense.cs | 20 ++++++++ src/SMAPI.Web/Framework/Clients/GitHub/GitRepo.cs | 20 ++++++++ .../Framework/Clients/GitHub/IGitHubClient.cs | 5 ++ .../Framework/ConfigModels/ApiClientsConfig.cs | 6 --- .../ModRepositories/ChucklefishRepository.cs | 12 ++--- .../Framework/ModRepositories/GitHubRepository.cs | 24 +++++++--- .../Framework/ModRepositories/ModDropRepository.cs | 10 ++-- .../Framework/ModRepositories/ModInfoModel.cs | 42 ++++++++++++++++- .../Framework/ModRepositories/NexusRepository.cs | 8 ++-- src/SMAPI.Web/Startup.cs | 2 - src/SMAPI.Web/appsettings.json | 2 - 18 files changed, 220 insertions(+), 77 deletions(-) create mode 100644 src/SMAPI.Web/Framework/Clients/GitHub/GitLicense.cs create mode 100644 src/SMAPI.Web/Framework/Clients/GitHub/GitRepo.cs (limited to 'src/SMAPI.Web/Framework') diff --git a/docs/release-notes.md b/docs/release-notes.md index b425112d..fd0cda51 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -26,6 +26,7 @@ These changes have not been released yet. * Fixed map reloads not updating door warps. * Fixed outdoor tilesheets being seasonalised when added to an indoor location. * Fixed update checks failing for Nexus mods marked as adult content. + * Fixed update checks not recognising releases on GitHub if they're not explicitly listed as update keys. * For the mod compatibility list: * Now loads faster (since data is fetched in a background service). diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs index fff86891..06c44308 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki { diff --git a/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs b/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs index 865ebcf7..3fc1759e 100644 --- a/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs +++ b/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs @@ -3,7 +3,7 @@ using System; namespace StardewModdingAPI.Toolkit.Framework.UpdateData { /// A namespaced mod ID which uniquely identifies a mod within a mod repository. - public class UpdateKey + public class UpdateKey : IEquatable { /********* ** Accessors @@ -38,6 +38,12 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData && !string.IsNullOrWhiteSpace(id); } + /// Construct an instance. + /// The mod repository containing the mod. + /// The mod ID within the repository. + public UpdateKey(ModRepositoryKey repository, string id) + : this($"{repository}:{id}", repository, id) { } + /// Parse a raw update key. /// The raw update key to parse. public static UpdateKey Parse(string raw) @@ -69,5 +75,29 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData ? $"{this.Repository}:{this.ID}" : this.RawText; } + + /// Indicates whether the current object is equal to another object of the same type. + /// An object to compare with this object. + public bool Equals(UpdateKey other) + { + return + other != null + && this.Repository == other.Repository + && string.Equals(this.ID, other.ID, StringComparison.InvariantCultureIgnoreCase); + } + + /// Determines whether the specified object is equal to the current object. + /// The object to compare with the current object. + public override bool Equals(object obj) + { + return obj is UpdateKey other && this.Equals(other); + } + + /// Serves as the default hash function. + /// A hash code for the current object. + public override int GetHashCode() + { + return $"{this.Repository}:{this.ID}".ToLower().GetHashCode(); + } } } diff --git a/src/SMAPI.Web/BackgroundService.cs b/src/SMAPI.Web/BackgroundService.cs index 2dd3b921..dfd2c1b9 100644 --- a/src/SMAPI.Web/BackgroundService.cs +++ b/src/SMAPI.Web/BackgroundService.cs @@ -50,11 +50,11 @@ namespace StardewModdingAPI.Web // set startup tasks BackgroundJob.Enqueue(() => BackgroundService.UpdateWikiAsync()); - BackgroundJob.Enqueue(() => BackgroundService.RemoveStaleMods()); + BackgroundJob.Enqueue(() => BackgroundService.RemoveStaleModsAsync()); // set recurring tasks RecurringJob.AddOrUpdate(() => BackgroundService.UpdateWikiAsync(), "*/10 * * * *"); // every 10 minutes - RecurringJob.AddOrUpdate(() => BackgroundService.RemoveStaleMods(), "0 * * * *"); // hourly + RecurringJob.AddOrUpdate(() => BackgroundService.RemoveStaleModsAsync(), "0 * * * *"); // hourly return Task.CompletedTask; } @@ -84,9 +84,10 @@ namespace StardewModdingAPI.Web } /// Remove mods which haven't been requested in over 48 hours. - public static async Task RemoveStaleMods() + public static Task RemoveStaleModsAsync() { BackgroundService.ModCache.RemoveStaleMods(TimeSpan.FromHours(48)); + return Task.CompletedTask; } diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 195ee5bf..13dd5529 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -123,18 +123,33 @@ namespace StardewModdingAPI.Web.Controllers // crossreference data ModDataRecord record = this.ModDatabase.Get(search.ID); WikiModEntry wikiEntry = wikiData.FirstOrDefault(entry => entry.ID.Contains(search.ID.Trim(), StringComparer.InvariantCultureIgnoreCase)); - string[] updateKeys = this.GetUpdateKeys(search.UpdateKeys, record, wikiEntry).ToArray(); + UpdateKey[] updateKeys = this.GetUpdateKeys(search.UpdateKeys, record, wikiEntry).ToArray(); + + // add soft lookups (don't log errors if the target doesn't exist) + UpdateKey[] softUpdateKeys = updateKeys.All(key => key.Repository != ModRepositoryKey.GitHub) && !string.IsNullOrWhiteSpace(wikiEntry?.GitHubRepo) + ? new[] { new UpdateKey(ModRepositoryKey.GitHub, wikiEntry.GitHubRepo) } + : new UpdateKey[0]; // get latest versions ModEntryModel result = new ModEntryModel { ID = search.ID }; IList errors = new List(); - foreach (string updateKey in updateKeys) + foreach (UpdateKey updateKey in updateKeys.Concat(softUpdateKeys)) { + bool isSoftLookup = softUpdateKeys.Contains(updateKey); + + // validate update key + if (!updateKey.LooksValid) + { + errors.Add($"The update key '{updateKey}' isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541'."); + continue; + } + // fetch data ModInfoModel data = await this.GetInfoForUpdateKeyAsync(updateKey); if (data.Error != null) { - errors.Add(data.Error); + if (!isSoftLookup || data.Status != RemoteModStatus.DoesNotExist) + errors.Add(data.Error); continue; } @@ -221,32 +236,27 @@ namespace StardewModdingAPI.Web.Controllers /// Get the mod info for an update key. /// The namespaced update key. - private async Task GetInfoForUpdateKeyAsync(string updateKey) + private async Task GetInfoForUpdateKeyAsync(UpdateKey updateKey) { - // parse update key - UpdateKey parsed = UpdateKey.Parse(updateKey); - if (!parsed.LooksValid) - return new ModInfoModel().WithError(RemoteModStatus.DoesNotExist, $"The update key '{updateKey}' isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541'."); - // get mod - if (!this.ModCache.TryGetMod(parsed.Repository, parsed.ID, out CachedMod mod) || this.ModCache.IsStale(mod.LastUpdated, mod.FetchStatus == RemoteModStatus.TemporaryError ? this.ErrorCacheMinutes : this.SuccessCacheMinutes)) + if (!this.ModCache.TryGetMod(updateKey.Repository, updateKey.ID, out CachedMod mod) || this.ModCache.IsStale(mod.LastUpdated, mod.FetchStatus == RemoteModStatus.TemporaryError ? this.ErrorCacheMinutes : this.SuccessCacheMinutes)) { // get site - if (!this.Repositories.TryGetValue(parsed.Repository, out IModRepository repository)) - return new ModInfoModel().WithError(RemoteModStatus.DoesNotExist, $"There's no mod site with key '{parsed.Repository}'. Expected one of [{string.Join(", ", this.Repositories.Keys)}]."); + if (!this.Repositories.TryGetValue(updateKey.Repository, out IModRepository repository)) + return new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, $"There's no mod site with key '{updateKey.Repository}'. Expected one of [{string.Join(", ", this.Repositories.Keys)}]."); // fetch mod - ModInfoModel result = await repository.GetModInfoAsync(parsed.ID); + ModInfoModel result = await repository.GetModInfoAsync(updateKey.ID); if (result.Error == null) { if (result.Version == null) - result.WithError(RemoteModStatus.InvalidData, $"The update key '{updateKey}' matches a mod with no version number."); + result.SetError(RemoteModStatus.InvalidData, $"The update key '{updateKey}' matches a mod with no version number."); else if (!SemanticVersion.TryParse(result.Version, out _)) - result.WithError(RemoteModStatus.InvalidData, $"The update key '{updateKey}' matches a mod with invalid semantic version '{result.Version}'."); + result.SetError(RemoteModStatus.InvalidData, $"The update key '{updateKey}' matches a mod with invalid semantic version '{result.Version}'."); } // cache mod - this.ModCache.SaveMod(repository.VendorKey, parsed.ID, result, out mod); + this.ModCache.SaveMod(repository.VendorKey, updateKey.ID, result, out mod); } return mod.GetModel(); } @@ -255,7 +265,7 @@ namespace StardewModdingAPI.Web.Controllers /// The specified update keys. /// The mod's entry in SMAPI's internal database. /// The mod's entry in the wiki list. - public IEnumerable GetUpdateKeys(string[] specifiedKeys, ModDataRecord record, WikiModEntry entry) + public IEnumerable GetUpdateKeys(string[] specifiedKeys, ModDataRecord record, WikiModEntry entry) { IEnumerable GetRaw() { @@ -283,10 +293,14 @@ namespace StardewModdingAPI.Web.Controllers } } - HashSet seen = new HashSet(StringComparer.InvariantCulture); - foreach (string key in GetRaw()) + HashSet seen = new HashSet(); + foreach (string rawKey in GetRaw()) { - if (!string.IsNullOrWhiteSpace(key) && seen.Add(key)) + if (string.IsNullOrWhiteSpace(rawKey)) + continue; + + UpdateKey key = UpdateKey.Parse(rawKey); + if (seen.Add(key)) yield return key; } } diff --git a/src/SMAPI.Web/Framework/Caching/Mods/CachedMod.cs b/src/SMAPI.Web/Framework/Caching/Mods/CachedMod.cs index fe8a7a1f..96eca847 100644 --- a/src/SMAPI.Web/Framework/Caching/Mods/CachedMod.cs +++ b/src/SMAPI.Web/Framework/Caching/Mods/CachedMod.cs @@ -58,6 +58,12 @@ namespace StardewModdingAPI.Web.Framework.Caching.Mods /// The URL for the mod page. public string Url { get; set; } + /// The license URL, if available. + public string LicenseUrl { get; set; } + + /// The license name, if available. + public string LicenseName { get; set; } + /********* ** Accessors @@ -86,12 +92,16 @@ namespace StardewModdingAPI.Web.Framework.Caching.Mods this.MainVersion = mod.Version; this.PreviewVersion = mod.PreviewVersion; this.Url = mod.Url; + this.LicenseUrl = mod.LicenseUrl; + this.LicenseName = mod.LicenseName; } /// Get the API model for the cached data. public ModInfoModel GetModel() { - return new ModInfoModel(name: this.Name, version: this.MainVersion, previewVersion: this.PreviewVersion, url: this.Url).WithError(this.FetchStatus, this.FetchError); + return new ModInfoModel(name: this.Name, version: this.MainVersion, url: this.Url, previewVersion: this.PreviewVersion) + .SetLicense(this.LicenseUrl, this.LicenseName) + .SetError(this.FetchStatus, this.FetchError); } } } diff --git a/src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs b/src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs index 22950db9..84c20957 100644 --- a/src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs +++ b/src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs @@ -12,12 +12,6 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub /********* ** Fields *********/ - /// 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; @@ -27,17 +21,12 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub *********/ /// Construct an instance. /// The base URL for the GitHub API. - /// 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 stableReleaseUrlFormat, string anyReleaseUrlFormat, string userAgent, string acceptHeader, string username, string password) + public GitHubClient(string baseUrl, string userAgent, string acceptHeader, string username, string password) { - this.StableReleaseUrlFormat = stableReleaseUrlFormat; - this.AnyReleaseUrlFormat = anyReleaseUrlFormat; - this.Client = new FluentClient(baseUrl) .SetUserAgent(userAgent) .AddDefault(req => req.WithHeader("Accept", acceptHeader)); @@ -45,25 +34,43 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub this.Client = this.Client.SetBasicAuthentication(username, password); } + /// Get basic metadata for a GitHub repository, if available. + /// The repository key (like Pathoschild/SMAPI). + /// Returns the repository info if it exists, else null. + public async Task GetRepositoryAsync(string repo) + { + this.AssertKeyFormat(repo); + try + { + return await this.Client + .GetAsync($"repos/{repo}") + .As(); + } + catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound) + { + return null; + } + } + /// Get the latest release for a GitHub repository. /// The repository key (like Pathoschild/SMAPI). /// 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) { - this.AssetKeyFormat(repo); + this.AssertKeyFormat(repo); try { if (includePrerelease) { GitRelease[] results = await this.Client - .GetAsync(string.Format(this.AnyReleaseUrlFormat, repo)) + .GetAsync($"repos/{repo}/releases?per_page=2") // allow for draft release (only visible if GitHub repo is owned by same account as the update check credentials) .AsArray(); return results.FirstOrDefault(p => !p.IsDraft); } return await this.Client - .GetAsync(string.Format(this.StableReleaseUrlFormat, repo)) + .GetAsync($"repos/{repo}/releases/latest") .As(); } catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound) @@ -85,7 +92,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub /// Assert that a repository key is formatted correctly. /// The repository key (like Pathoschild/SMAPI). /// The repository key is invalid. - private void AssetKeyFormat(string repo) + private void AssertKeyFormat(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/GitLicense.cs b/src/SMAPI.Web/Framework/Clients/GitHub/GitLicense.cs new file mode 100644 index 00000000..736efbe6 --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/GitHub/GitLicense.cs @@ -0,0 +1,20 @@ +using Newtonsoft.Json; + +namespace StardewModdingAPI.Web.Framework.Clients.GitHub +{ + /// The license info for a GitHub project. + internal class GitLicense + { + /// The license display name. + [JsonProperty("name")] + public string Name { get; set; } + + /// The SPDX ID for the license. + [JsonProperty("spdx_id")] + public string SpdxId { get; set; } + + /// The URL for the license info. + [JsonProperty("url")] + public string Url { get; set; } + } +} diff --git a/src/SMAPI.Web/Framework/Clients/GitHub/GitRepo.cs b/src/SMAPI.Web/Framework/Clients/GitHub/GitRepo.cs new file mode 100644 index 00000000..7d80576e --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/GitHub/GitRepo.cs @@ -0,0 +1,20 @@ +using Newtonsoft.Json; + +namespace StardewModdingAPI.Web.Framework.Clients.GitHub +{ + /// Basic metadata about a GitHub project. + internal class GitRepo + { + /// The full repository name, including the owner. + [JsonProperty("full_name")] + public string FullName { get; set; } + + /// The URL to the repository web page, if any. + [JsonProperty("html_url")] + public string WebUrl { get; set; } + + /// The code license, if any. + [JsonProperty("license")] + public GitLicense License { get; set; } + } +} diff --git a/src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs b/src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs index 9519c26f..a34f03bd 100644 --- a/src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs +++ b/src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs @@ -9,6 +9,11 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub /********* ** Methods *********/ + /// Get basic metadata for a GitHub repository, if available. + /// The repository key (like Pathoschild/SMAPI). + /// Returns the repository info if it exists, else null. + Task GetRepositoryAsync(string repo); + /// Get the latest release for a GitHub repository. /// The repository key (like Pathoschild/SMAPI). /// Whether to return a prerelease version if it's latest. diff --git a/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs index d2e9a2fe..a0a1f42a 100644 --- a/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs +++ b/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs @@ -29,12 +29,6 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels /// The base URL for the GitHub API. public string GitHubBaseUrl { 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 04c80dd2..c14fb45d 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs @@ -32,21 +32,19 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories { // validate ID format if (!uint.TryParse(id, out uint realID)) - return new ModInfoModel().WithError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid Chucklefish mod ID, must be an integer ID."); + return new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid Chucklefish mod ID, must be an integer ID."); // fetch info try { var mod = await this.Client.GetModAsync(realID); - if (mod == null) - return new ModInfoModel().WithError(RemoteModStatus.DoesNotExist, "Found no Chucklefish mod with this ID."); - - // create model - return new ModInfoModel(name: mod.Name, version: this.NormaliseVersion(mod.Version), url: mod.Url); + return mod != null + ? new ModInfoModel(name: mod.Name, version: this.NormaliseVersion(mod.Version), url: mod.Url) + : new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, "Found no Chucklefish mod with this ID."); } catch (Exception ex) { - return new ModInfoModel().WithError(RemoteModStatus.TemporaryError, ex.ToString()); + return new ModInfoModel().SetError(RemoteModStatus.TemporaryError, ex.ToString()); } } diff --git a/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs index 614e00c2..0e254b39 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs @@ -30,36 +30,46 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories /// The mod ID in this repository. public override async Task GetModInfoAsync(string id) { + ModInfoModel result = new ModInfoModel().SetBasicInfo(id, $"https://github.com/{id}/releases"); + // validate ID format if (!id.Contains("/") || id.IndexOf("/", StringComparison.InvariantCultureIgnoreCase) != id.LastIndexOf("/", StringComparison.InvariantCultureIgnoreCase)) - return new ModInfoModel().WithError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid GitHub mod ID, must be a username and project name like 'Pathoschild/LookupAnything'."); + return result.SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid GitHub mod ID, must be a username and project name like 'Pathoschild/LookupAnything'."); // fetch info try { + // fetch repo info + GitRepo repository = await this.Client.GetRepositoryAsync(id); + if (repository == null) + return result.SetError(RemoteModStatus.DoesNotExist, "Found no GitHub repository for this ID."); + result + .SetBasicInfo(repository.FullName, $"{repository.WebUrl}/releases") + .SetLicense(url: repository.License?.Url, name: repository.License?.Name); + // get latest release (whether preview or stable) GitRelease latest = await this.Client.GetLatestReleaseAsync(id, includePrerelease: true); if (latest == null) - return new ModInfoModel().WithError(RemoteModStatus.DoesNotExist, "Found no GitHub release for this ID."); + return result.SetError(RemoteModStatus.DoesNotExist, "Found no GitHub release for this ID."); // split stable/prerelease if applicable GitRelease preview = null; if (latest.IsPrerelease) { - GitRelease result = await this.Client.GetLatestReleaseAsync(id, includePrerelease: false); - if (result != null) + GitRelease release = await this.Client.GetLatestReleaseAsync(id, includePrerelease: false); + if (release != null) { preview = latest; - latest = result; + latest = release; } } // return data - return new ModInfoModel(name: id, version: this.NormaliseVersion(latest.Tag), previewVersion: this.NormaliseVersion(preview?.Tag), url: $"https://github.com/{id}/releases"); + return result.SetVersions(version: this.NormaliseVersion(latest.Tag), previewVersion: this.NormaliseVersion(preview?.Tag)); } catch (Exception ex) { - return new ModInfoModel().WithError(RemoteModStatus.TemporaryError, ex.ToString()); + return result.SetError(RemoteModStatus.TemporaryError, ex.ToString()); } } diff --git a/src/SMAPI.Web/Framework/ModRepositories/ModDropRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/ModDropRepository.cs index 5703b34e..62142668 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/ModDropRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/ModDropRepository.cs @@ -32,19 +32,19 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories { // validate ID format if (!long.TryParse(id, out long modDropID)) - return new ModInfoModel().WithError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid ModDrop mod ID, must be an integer ID."); + return new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid ModDrop mod ID, must be an integer ID."); // fetch info try { ModDropMod mod = await this.Client.GetModAsync(modDropID); - if (mod == null) - return new ModInfoModel().WithError(RemoteModStatus.DoesNotExist, "Found no ModDrop mod with this ID."); - return new ModInfoModel(name: mod.Name, version: mod.LatestDefaultVersion?.ToString(), previewVersion: mod.LatestOptionalVersion?.ToString(), url: mod.Url); + return mod != null + ? new ModInfoModel(name: mod.Name, version: mod.LatestDefaultVersion?.ToString(), previewVersion: mod.LatestOptionalVersion?.ToString(), url: mod.Url) + : new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, "Found no ModDrop mod with this ID."); } catch (Exception ex) { - return new ModInfoModel().WithError(RemoteModStatus.TemporaryError, ex.ToString()); + return new ModInfoModel().SetError(RemoteModStatus.TemporaryError, ex.ToString()); } } diff --git a/src/SMAPI.Web/Framework/ModRepositories/ModInfoModel.cs b/src/SMAPI.Web/Framework/ModRepositories/ModInfoModel.cs index 15e6c213..46b98860 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/ModInfoModel.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/ModInfoModel.cs @@ -18,6 +18,12 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories /// The mod's web URL. public string Url { get; set; } + /// The license URL, if available. + public string LicenseUrl { get; set; } + + /// The license name, if available. + public string LicenseName { get; set; } + /// The mod availability status on the remote site. public RemoteModStatus Status { get; set; } = RemoteModStatus.Ok; @@ -37,17 +43,49 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories /// The semantic version for the mod's latest preview release, if available and different from . /// The mod's web URL. public ModInfoModel(string name, string version, string url, string previewVersion = null) + { + this + .SetBasicInfo(name, url) + .SetVersions(version, previewVersion); + } + + /// Set the basic mod info. + /// The mod name. + /// The mod's web URL. + public ModInfoModel SetBasicInfo(string name, string url) { this.Name = name; + this.Url = url; + + return this; + } + + /// Set the mod version info. + /// The semantic version for the mod's latest release. + /// The semantic version for the mod's latest preview release, if available and different from . + public ModInfoModel SetVersions(string version, string previewVersion = null) + { this.Version = version; this.PreviewVersion = previewVersion; - this.Url = url; + + return this; + } + + /// Set the license info, if available. + /// The license URL. + /// The license name. + public ModInfoModel SetLicense(string url, string name) + { + this.LicenseUrl = url; + this.LicenseName = name; + + return this; } /// Set a mod error. /// The mod availability status on the remote site. /// The error message indicating why the mod is invalid (if applicable). - public ModInfoModel WithError(RemoteModStatus status, string error) + public ModInfoModel SetError(RemoteModStatus status, string error) { this.Status = status; this.Error = error; diff --git a/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs index a4ae61eb..b4791f56 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs @@ -32,27 +32,27 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories { // validate ID format if (!uint.TryParse(id, out uint nexusID)) - return new ModInfoModel().WithError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid Nexus mod ID, must be an integer ID."); + return new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid Nexus mod ID, must be an integer ID."); // fetch info try { NexusMod mod = await this.Client.GetModAsync(nexusID); if (mod == null) - return new ModInfoModel().WithError(RemoteModStatus.DoesNotExist, "Found no Nexus mod with this ID."); + return new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, "Found no Nexus mod with this ID."); if (mod.Error != null) { RemoteModStatus remoteStatus = mod.Status == NexusModStatus.Hidden || mod.Status == NexusModStatus.NotPublished ? RemoteModStatus.DoesNotExist : RemoteModStatus.TemporaryError; - return new ModInfoModel().WithError(remoteStatus, mod.Error); + return new ModInfoModel().SetError(remoteStatus, mod.Error); } return new ModInfoModel(name: mod.Name, version: this.NormaliseVersion(mod.Version), previewVersion: mod.LatestFileVersion?.ToString(), url: mod.Url); } catch (Exception ex) { - return new ModInfoModel().WithError(RemoteModStatus.TemporaryError, ex.ToString()); + return new ModInfoModel().SetError(RemoteModStatus.TemporaryError, ex.ToString()); } } diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index 4d23fe65..33737235 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -121,8 +121,6 @@ namespace StardewModdingAPI.Web services.AddSingleton(new GitHubClient( baseUrl: api.GitHubBaseUrl, - 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 3ea37dea..f9777f87 100644 --- a/src/SMAPI.Web/appsettings.json +++ b/src/SMAPI.Web/appsettings.json @@ -30,8 +30,6 @@ "ChucklefishModPageUrlFormat": "resources/{0}", "GitHubBaseUrl": "https://api.github.com", - "GitHubStableReleaseUrlFormat": "repos/{0}/releases/latest", - "GitHubAnyReleaseUrlFormat": "repos/{0}/releases?per_page=2", // allow for draft release (only visible if GitHub repo is owned by same account as the update check credentials) "GitHubAcceptHeader": "application/vnd.github.v3+json", "GitHubUsername": null, // see top note "GitHubPassword": null, // see top note -- cgit From 4bb644e46e579c7d49306cbb0692521e3027264b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 29 Jul 2019 20:59:19 -0400 Subject: prefer SPDX license ID in tracked license info (#651) --- src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/SMAPI.Web/Framework') diff --git a/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs index 0e254b39..e06a2497 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs @@ -45,7 +45,7 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories return result.SetError(RemoteModStatus.DoesNotExist, "Found no GitHub repository for this ID."); result .SetBasicInfo(repository.FullName, $"{repository.WebUrl}/releases") - .SetLicense(url: repository.License?.Url, name: repository.License?.Name); + .SetLicense(url: repository.License?.Url, name: repository.License?.SpdxId ?? repository.License?.Name); // get latest release (whether preview or stable) GitRelease latest = await this.Client.GetLatestReleaseAsync(id, includePrerelease: true); -- cgit From 85715988f987a2bf1e7016e9ad82e76ec44d4f94 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 31 Jul 2019 14:49:54 -0400 Subject: fix error when Chucklefish page doesn't exist for update checks --- docs/release-notes.md | 6 ++++-- src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) (limited to 'src/SMAPI.Web/Framework') diff --git a/docs/release-notes.md b/docs/release-notes.md index fd0cda51..9ee8ae8f 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -15,6 +15,10 @@ These changes have not been released yet. * Save Backup now works in the background, to avoid affecting startup time for players with a large number of saves. * Duplicate-mod errors now show the mod version in each folder. * Updated mod compatibility list. + * Improved update checks: + * Update checks are now faster in some cases. + * Fixed error if a Nexus mod is marked as adult content. + * Fixed error if the Chucklefish page for an update key doesn't exist. * Fixed mods needing to load custom `Map` assets before the game accesses them (SMAPI will now do so automatically). * Fixed Save Backup not pruning old backups if they're uncompressed. * Fixed issues when a farmhand reconnects before the game notices they're disconnected. @@ -25,8 +29,6 @@ These changes have not been released yet. * Fixed map reloads resetting tilesheet seasons. * Fixed map reloads not updating door warps. * Fixed outdoor tilesheets being seasonalised when added to an indoor location. - * Fixed update checks failing for Nexus mods marked as adult content. - * Fixed update checks not recognising releases on GitHub if they're not explicitly listed as update keys. * For the mod compatibility list: * Now loads faster (since data is fetched in a background service). diff --git a/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs b/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs index 2753e33a..939c32c6 100644 --- a/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs +++ b/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs @@ -45,7 +45,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish .GetAsync(string.Format(this.ModPageUrlFormat, id)) .AsString(); } - catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound) + catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound || ex.Status == HttpStatusCode.Forbidden) { return null; } -- cgit From c86db64880d52630c372aa24f7aadf0036fb3fcf Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 4 Aug 2019 16:37:10 -0400 Subject: encapsulate gzip logic for reuse (#654) --- src/SMAPI.Web/Controllers/LogParserController.cs | 82 ++------------------ src/SMAPI.Web/Framework/Compression/GzipHelper.cs | 89 ++++++++++++++++++++++ src/SMAPI.Web/Framework/Compression/IGzipHelper.cs | 17 +++++ src/SMAPI.Web/Startup.cs | 4 + 4 files changed, 118 insertions(+), 74 deletions(-) create mode 100644 src/SMAPI.Web/Framework/Compression/GzipHelper.cs create mode 100644 src/SMAPI.Web/Framework/Compression/IGzipHelper.cs (limited to 'src/SMAPI.Web/Framework') diff --git a/src/SMAPI.Web/Controllers/LogParserController.cs b/src/SMAPI.Web/Controllers/LogParserController.cs index 21e4a56f..dc5895b0 100644 --- a/src/SMAPI.Web/Controllers/LogParserController.cs +++ b/src/SMAPI.Web/Controllers/LogParserController.cs @@ -1,13 +1,11 @@ using System; -using System.IO; -using System.IO.Compression; using System.Linq; -using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using StardewModdingAPI.Web.Framework; using StardewModdingAPI.Web.Framework.Clients.Pastebin; +using StardewModdingAPI.Web.Framework.Compression; using StardewModdingAPI.Web.Framework.ConfigModels; using StardewModdingAPI.Web.Framework.LogParsing; using StardewModdingAPI.Web.Framework.LogParsing.Models; @@ -27,9 +25,8 @@ namespace StardewModdingAPI.Web.Controllers /// The underlying Pastebin client. private readonly IPastebinClient Pastebin; - /// The first bytes in a valid zip file. - /// See . - private const uint GzipLeadBytes = 0x8b1f; + /// The underlying text compression helper. + private readonly IGzipHelper GzipHelper; /********* @@ -41,10 +38,12 @@ namespace StardewModdingAPI.Web.Controllers /// Construct an instance. /// The context config settings. /// The Pastebin API client. - public LogParserController(IOptions siteConfig, IPastebinClient pastebin) + /// The underlying text compression helper. + public LogParserController(IOptions siteConfig, IPastebinClient pastebin, IGzipHelper gzipHelper) { this.Config = siteConfig.Value; this.Pastebin = pastebin; + this.GzipHelper = gzipHelper; } /*** @@ -84,7 +83,7 @@ namespace StardewModdingAPI.Web.Controllers return this.View("Index", new LogParserModel(this.Config.LogParserUrl, null) { UploadError = "The log file seems to be empty." }); // upload log - input = this.CompressString(input); + input = this.GzipHelper.CompressString(input); SavePasteResult result = await this.Pastebin.PostAsync(input); // handle errors @@ -106,75 +105,10 @@ namespace StardewModdingAPI.Web.Controllers private async Task GetAsync(string id) { PasteInfo response = await this.Pastebin.GetAsync(id); - response.Content = this.DecompressString(response.Content); + response.Content = this.GzipHelper.DecompressString(response.Content); return response; } - /// Compress a string. - /// The text to compress. - /// Derived from . - private string CompressString(string text) - { - // get raw bytes - byte[] buffer = Encoding.UTF8.GetBytes(text); - - // compressed - byte[] compressedData; - using (MemoryStream stream = new MemoryStream()) - { - using (GZipStream zipStream = new GZipStream(stream, CompressionLevel.Optimal, leaveOpen: true)) - zipStream.Write(buffer, 0, buffer.Length); - - stream.Position = 0; - compressedData = new byte[stream.Length]; - stream.Read(compressedData, 0, compressedData.Length); - } - - // prefix length - byte[] zipBuffer = new byte[compressedData.Length + 4]; - Buffer.BlockCopy(compressedData, 0, zipBuffer, 4, compressedData.Length); - Buffer.BlockCopy(BitConverter.GetBytes(buffer.Length), 0, zipBuffer, 0, 4); - - // return string representation - return Convert.ToBase64String(zipBuffer); - } - /// Decompress a string. - /// The compressed text. - /// Derived from . - private string DecompressString(string rawText) - { - // get raw bytes - byte[] zipBuffer; - try - { - zipBuffer = Convert.FromBase64String(rawText); - } - catch - { - return rawText; // not valid base64, wasn't compressed by the log parser - } - - // skip if not gzip - if (BitConverter.ToUInt16(zipBuffer, 4) != LogParserController.GzipLeadBytes) - return rawText; - - // decompress - using (MemoryStream memoryStream = new MemoryStream()) - { - // read length prefix - int dataLength = BitConverter.ToInt32(zipBuffer, 0); - memoryStream.Write(zipBuffer, 4, zipBuffer.Length - 4); - - // read data - byte[] buffer = new byte[dataLength]; - memoryStream.Position = 0; - using (GZipStream gZipStream = new GZipStream(memoryStream, CompressionMode.Decompress)) - gZipStream.Read(buffer, 0, buffer.Length); - - // return original string - return Encoding.UTF8.GetString(buffer); - } - } } } diff --git a/src/SMAPI.Web/Framework/Compression/GzipHelper.cs b/src/SMAPI.Web/Framework/Compression/GzipHelper.cs new file mode 100644 index 00000000..cc8f4737 --- /dev/null +++ b/src/SMAPI.Web/Framework/Compression/GzipHelper.cs @@ -0,0 +1,89 @@ +using System; +using System.IO; +using System.IO.Compression; +using System.Text; + +namespace StardewModdingAPI.Web.Framework.Compression +{ + /// Handles GZip compression logic. + internal class GzipHelper : IGzipHelper + { + /********* + ** Fields + *********/ + /// The first bytes in a valid zip file. + /// See . + private const uint GzipLeadBytes = 0x8b1f; + + + /********* + ** Public methods + *********/ + /// Compress a string. + /// The text to compress. + /// Derived from . + public string CompressString(string text) + { + // get raw bytes + byte[] buffer = Encoding.UTF8.GetBytes(text); + + // compressed + byte[] compressedData; + using (MemoryStream stream = new MemoryStream()) + { + using (GZipStream zipStream = new GZipStream(stream, CompressionLevel.Optimal, leaveOpen: true)) + zipStream.Write(buffer, 0, buffer.Length); + + stream.Position = 0; + compressedData = new byte[stream.Length]; + stream.Read(compressedData, 0, compressedData.Length); + } + + // prefix length + byte[] zipBuffer = new byte[compressedData.Length + 4]; + Buffer.BlockCopy(compressedData, 0, zipBuffer, 4, compressedData.Length); + Buffer.BlockCopy(BitConverter.GetBytes(buffer.Length), 0, zipBuffer, 0, 4); + + // return string representation + return Convert.ToBase64String(zipBuffer); + } + + /// Decompress a string. + /// The compressed text. + /// Derived from . + public string DecompressString(string rawText) + { + // get raw bytes + byte[] zipBuffer; + try + { + zipBuffer = Convert.FromBase64String(rawText); + } + catch + { + return rawText; // not valid base64, wasn't compressed by the log parser + } + + // skip if not gzip + if (BitConverter.ToUInt16(zipBuffer, 4) != GzipHelper.GzipLeadBytes) + return rawText; + + // decompress + using (MemoryStream memoryStream = new MemoryStream()) + { + // read length prefix + int dataLength = BitConverter.ToInt32(zipBuffer, 0); + memoryStream.Write(zipBuffer, 4, zipBuffer.Length - 4); + + // read data + byte[] buffer = new byte[dataLength]; + memoryStream.Position = 0; + using (GZipStream gZipStream = new GZipStream(memoryStream, CompressionMode.Decompress)) + gZipStream.Read(buffer, 0, buffer.Length); + + // return original string + return Encoding.UTF8.GetString(buffer); + } + } + } +} diff --git a/src/SMAPI.Web/Framework/Compression/IGzipHelper.cs b/src/SMAPI.Web/Framework/Compression/IGzipHelper.cs new file mode 100644 index 00000000..a000865e --- /dev/null +++ b/src/SMAPI.Web/Framework/Compression/IGzipHelper.cs @@ -0,0 +1,17 @@ +namespace StardewModdingAPI.Web.Framework.Compression +{ + /// Handles GZip compression logic. + internal interface IGzipHelper + { + /********* + ** Methods + *********/ + /// Compress a string. + /// The text to compress. + string CompressString(string text); + + /// Decompress a string. + /// The compressed text. + string DecompressString(string rawText); + } +} diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index 33737235..bb43f5a5 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -20,6 +20,7 @@ using StardewModdingAPI.Web.Framework.Clients.GitHub; using StardewModdingAPI.Web.Framework.Clients.ModDrop; using StardewModdingAPI.Web.Framework.Clients.Nexus; using StardewModdingAPI.Web.Framework.Clients.Pastebin; +using StardewModdingAPI.Web.Framework.Compression; using StardewModdingAPI.Web.Framework.ConfigModels; using StardewModdingAPI.Web.Framework.RewriteRules; @@ -149,6 +150,9 @@ namespace StardewModdingAPI.Web devKey: api.PastebinDevKey )); } + + // init helpers + services.AddSingleton(new GzipHelper()); } /// The method called by the runtime to configure the HTTP request pipeline. -- cgit From 3ba567eaddeaa0bb2bdd749b56e0601d1cf65a25 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 4 Aug 2019 03:28:34 -0400 Subject: add JSON validator with initial support for manifest format (#654) --- .../Controllers/JsonValidatorController.cs | 217 +++++++++++++++++++++ src/SMAPI.Web/Controllers/LogParserController.cs | 4 +- .../Framework/Clients/Pastebin/IPastebinClient.cs | 3 +- .../Framework/Clients/Pastebin/PastebinClient.cs | 5 +- src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs | 3 + src/SMAPI.Web/SMAPI.Web.csproj | 1 + src/SMAPI.Web/Startup.cs | 2 +- .../JsonValidator/JsonValidatorErrorModel.cs | 36 ++++ .../ViewModels/JsonValidator/JsonValidatorModel.cs | 92 +++++++++ .../JsonValidator/JsonValidatorRequestModel.cs | 15 ++ src/SMAPI.Web/Views/JsonValidator/Index.cshtml | 126 ++++++++++++ src/SMAPI.Web/Views/Shared/_Layout.cshtml | 7 +- src/SMAPI.Web/appsettings.Development.json | 1 + src/SMAPI.Web/appsettings.json | 1 + .../wwwroot/Content/css/json-validator.css | 98 ++++++++++ src/SMAPI.Web/wwwroot/Content/css/main.css | 2 +- src/SMAPI.Web/wwwroot/Content/js/json-validator.js | 60 ++++++ src/SMAPI.Web/wwwroot/Content/js/log-parser.js | 17 +- src/SMAPI.Web/wwwroot/schemas/manifest.json | 120 ++++++++++++ 19 files changed, 792 insertions(+), 18 deletions(-) create mode 100644 src/SMAPI.Web/Controllers/JsonValidatorController.cs create mode 100644 src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorErrorModel.cs create mode 100644 src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorModel.cs create mode 100644 src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorRequestModel.cs create mode 100644 src/SMAPI.Web/Views/JsonValidator/Index.cshtml create mode 100644 src/SMAPI.Web/wwwroot/Content/css/json-validator.css create mode 100644 src/SMAPI.Web/wwwroot/Content/js/json-validator.js create mode 100644 src/SMAPI.Web/wwwroot/schemas/manifest.json (limited to 'src/SMAPI.Web/Framework') diff --git a/src/SMAPI.Web/Controllers/JsonValidatorController.cs b/src/SMAPI.Web/Controllers/JsonValidatorController.cs new file mode 100644 index 00000000..9d1685ac --- /dev/null +++ b/src/SMAPI.Web/Controllers/JsonValidatorController.cs @@ -0,0 +1,217 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Schema; +using StardewModdingAPI.Web.Framework; +using StardewModdingAPI.Web.Framework.Clients.Pastebin; +using StardewModdingAPI.Web.Framework.Compression; +using StardewModdingAPI.Web.Framework.ConfigModels; +using StardewModdingAPI.Web.ViewModels.JsonValidator; + +namespace StardewModdingAPI.Web.Controllers +{ + /// Provides a web UI for validating JSON schemas. + internal class JsonValidatorController : Controller + { + /********* + ** Fields + *********/ + /// The site config settings. + private readonly SiteConfig Config; + + /// The underlying Pastebin client. + private readonly IPastebinClient Pastebin; + + /// The underlying text compression helper. + private readonly IGzipHelper GzipHelper; + + /// The section URL for the schema validator. + private string SectionUrl => this.Config.JsonValidatorUrl; + + /// The supported JSON schemas (names indexed by ID). + private readonly IDictionary SchemaFormats = new Dictionary + { + ["none"] = "None", + ["manifest"] = "Manifest" + }; + + /// The schema ID to use if none was specified. + private string DefaultSchemaID = "manifest"; + + + /********* + ** Public methods + *********/ + /*** + ** Constructor + ***/ + /// Construct an instance. + /// The context config settings. + /// The Pastebin API client. + /// The underlying text compression helper. + public JsonValidatorController(IOptions siteConfig, IPastebinClient pastebin, IGzipHelper gzipHelper) + { + this.Config = siteConfig.Value; + this.Pastebin = pastebin; + this.GzipHelper = gzipHelper; + } + + /*** + ** Web UI + ***/ + /// Render the schema validator UI. + /// The schema name with which to validate the JSON. + /// The paste ID. + [HttpGet] + [Route("json")] + [Route("json/{schemaName}")] + [Route("json/{schemaName}/{id}")] + public async Task Index(string schemaName = null, string id = null) + { + schemaName = this.NormaliseSchemaName(schemaName); + + var result = new JsonValidatorModel(this.SectionUrl, id, schemaName, this.SchemaFormats); + if (string.IsNullOrWhiteSpace(id)) + return this.View("Index", result); + + // fetch raw JSON + PasteInfo paste = await this.GetAsync(id); + if (string.IsNullOrWhiteSpace(paste.Content)) + return this.View("Index", result.SetUploadError("The JSON file seems to be empty.")); + result.SetContent(paste.Content); + + // parse JSON + JToken parsed; + try + { + parsed = JToken.Parse(paste.Content); + } + catch (JsonReaderException ex) + { + return this.View("Index", result.AddErrors(new JsonValidatorErrorModel(ex.LineNumber, ex.Path, ex.Message))); + } + + // skip if no schema selected + if (schemaName == "none") + return this.View("Index", result); + + // load schema + JSchema schema; + { + FileInfo schemaFile = this.FindSchemaFile(schemaName); + if (schemaFile == null) + return this.View("Index", result.SetParseError($"Invalid schema '{schemaName}'.")); + schema = JSchema.Parse(System.IO.File.ReadAllText(schemaFile.FullName)); + } + + // validate JSON + parsed.IsValid(schema, out IList rawErrors); + var errors = rawErrors + .Select(error => new JsonValidatorErrorModel(error.LineNumber, error.Path, this.GetFlattenedError(error))) + .ToArray(); + return this.View("Index", result.AddErrors(errors)); + } + + /*** + ** JSON + ***/ + /// Save raw JSON data. + [HttpPost, AllowLargePosts] + [Route("json")] + public async Task PostAsync(JsonValidatorRequestModel request) + { + if (request == null) + return this.View("Index", new JsonValidatorModel(this.SectionUrl, null, null, this.SchemaFormats).SetUploadError("The request seems to be invalid.")); + + // normalise schema name + string schemaName = this.NormaliseSchemaName(request.SchemaName); + + // get raw log text + string input = request.Content; + if (string.IsNullOrWhiteSpace(input)) + return this.View("Index", new JsonValidatorModel(this.SectionUrl, null, schemaName, this.SchemaFormats).SetUploadError("The JSON file seems to be empty.")); + + // upload log + input = this.GzipHelper.CompressString(input); + SavePasteResult result = await this.Pastebin.PostAsync($"JSON validator {DateTime.UtcNow:s}", input); + + // handle errors + if (!result.Success) + return this.View("Index", new JsonValidatorModel(this.SectionUrl, result.ID, schemaName, this.SchemaFormats).SetUploadError($"Pastebin error: {result.Error ?? "unknown error"}")); + + // redirect to view + UriBuilder uri = new UriBuilder(new Uri(this.SectionUrl)); + uri.Path = $"{uri.Path.TrimEnd('/')}/{schemaName}/{result.ID}"; + return this.Redirect(uri.Uri.ToString()); + } + + + /********* + ** Private methods + *********/ + /// Fetch raw text from Pastebin. + /// The Pastebin paste ID. + private async Task GetAsync(string id) + { + PasteInfo response = await this.Pastebin.GetAsync(id); + response.Content = this.GzipHelper.DecompressString(response.Content); + return response; + } + + /// Get a flattened, human-readable message representing a schema validation error. + /// The error to represent. + /// The indentation level to apply for inner errors. + private string GetFlattenedError(ValidationError error, int indent = 0) + { + // get friendly representation of main error + string message = error.Message; + switch (error.ErrorType) + { + case ErrorType.Enum: + message = $"Invalid value. Found '{error.Value}', but expected one of '{string.Join("', '", error.Schema.Enum)}'."; + break; + } + + // add inner errors + foreach (ValidationError childError in error.ChildErrors) + message += "\n" + "".PadLeft(indent * 2, ' ') + $"==> {childError.Path}: " + this.GetFlattenedError(childError, indent + 1); + return message; + } + + /// Get a normalised schema name, or the if blank. + /// The raw schema name to normalise. + private string NormaliseSchemaName(string schemaName) + { + schemaName = schemaName?.Trim().ToLower(); + return !string.IsNullOrWhiteSpace(schemaName) + ? schemaName + : this.DefaultSchemaID; + } + + /// Get the schema file given its unique ID. + /// The schema ID. + private FileInfo FindSchemaFile(string id) + { + // normalise ID + id = id?.Trim().ToLower(); + if (string.IsNullOrWhiteSpace(id)) + return null; + + // get matching file + DirectoryInfo schemaDir = new DirectoryInfo(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "schemas")); + foreach (FileInfo file in schemaDir.EnumerateFiles("*.json")) + { + if (file.Name.Equals($"{id}.json")) + return file; + } + + return null; + } + } +} diff --git a/src/SMAPI.Web/Controllers/LogParserController.cs b/src/SMAPI.Web/Controllers/LogParserController.cs index dc5895b0..0556a81e 100644 --- a/src/SMAPI.Web/Controllers/LogParserController.cs +++ b/src/SMAPI.Web/Controllers/LogParserController.cs @@ -84,7 +84,7 @@ namespace StardewModdingAPI.Web.Controllers // upload log input = this.GzipHelper.CompressString(input); - SavePasteResult result = await this.Pastebin.PostAsync(input); + SavePasteResult result = await this.Pastebin.PostAsync($"SMAPI log {DateTime.UtcNow:s}", input); // handle errors if (!result.Success) @@ -108,7 +108,5 @@ namespace StardewModdingAPI.Web.Controllers response.Content = this.GzipHelper.DecompressString(response.Content); return response; } - - } } diff --git a/src/SMAPI.Web/Framework/Clients/Pastebin/IPastebinClient.cs b/src/SMAPI.Web/Framework/Clients/Pastebin/IPastebinClient.cs index 630dfb76..a635abe3 100644 --- a/src/SMAPI.Web/Framework/Clients/Pastebin/IPastebinClient.cs +++ b/src/SMAPI.Web/Framework/Clients/Pastebin/IPastebinClient.cs @@ -11,7 +11,8 @@ namespace StardewModdingAPI.Web.Framework.Clients.Pastebin Task GetAsync(string id); /// Save a paste to Pastebin. + /// The paste name. /// The paste content. - Task PostAsync(string content); + Task PostAsync(string name, string content); } } diff --git a/src/SMAPI.Web/Framework/Clients/Pastebin/PastebinClient.cs b/src/SMAPI.Web/Framework/Clients/Pastebin/PastebinClient.cs index 1e46f2dc..2e8a8c68 100644 --- a/src/SMAPI.Web/Framework/Clients/Pastebin/PastebinClient.cs +++ b/src/SMAPI.Web/Framework/Clients/Pastebin/PastebinClient.cs @@ -67,8 +67,9 @@ namespace StardewModdingAPI.Web.Framework.Clients.Pastebin } /// Save a paste to Pastebin. + /// The paste name. /// The paste content. - public async Task PostAsync(string content) + public async Task PostAsync(string name, string content) { try { @@ -85,7 +86,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.Pastebin api_user_key = this.UserKey, api_dev_key = this.DevKey, api_paste_private = 1, // unlisted - api_paste_name = $"SMAPI log {DateTime.UtcNow:s}", + api_paste_name = name, api_paste_expire_date = "N", // never expire api_paste_code = content })) diff --git a/src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs index d89a4260..bc6e868a 100644 --- a/src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs +++ b/src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs @@ -12,6 +12,9 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels /// The root URL for the log parser. public string LogParserUrl { get; set; } + /// The root URL for the JSON validator. + public string JsonValidatorUrl { get; set; } + /// The root URL for the mod list. public string ModListUrl { get; set; } diff --git a/src/SMAPI.Web/SMAPI.Web.csproj b/src/SMAPI.Web/SMAPI.Web.csproj index 26b29fce..98517818 100644 --- a/src/SMAPI.Web/SMAPI.Web.csproj +++ b/src/SMAPI.Web/SMAPI.Web.csproj @@ -26,6 +26,7 @@ + diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index bb43f5a5..de45b8a4 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -203,7 +203,7 @@ namespace StardewModdingAPI.Web redirects.Add(new ConditionalRewriteSubdomainRule( shouldRewrite: req => req.Host.Host != "localhost" - && (req.Host.Host.StartsWith("api.") || req.Host.Host.StartsWith("log.") || req.Host.Host.StartsWith("mods.")) + && (req.Host.Host.StartsWith("api.") || req.Host.Host.StartsWith("json.") || req.Host.Host.StartsWith("log.") || req.Host.Host.StartsWith("mods.")) && !req.Path.StartsWithSegments("/content") && !req.Path.StartsWithSegments("/favicon.ico") )); diff --git a/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorErrorModel.cs b/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorErrorModel.cs new file mode 100644 index 00000000..f9497a38 --- /dev/null +++ b/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorErrorModel.cs @@ -0,0 +1,36 @@ +namespace StardewModdingAPI.Web.ViewModels.JsonValidator +{ + /// The view model for a JSON validator error. + public class JsonValidatorErrorModel + { + /********* + ** Accessors + *********/ + /// The line number on which the error occurred. + public int Line { get; set; } + + /// The field path in the JSON file where the error occurred. + public string Path { get; set; } + + /// A human-readable description of the error. + public string Message { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public JsonValidatorErrorModel() { } + + /// Construct an instance. + /// The line number on which the error occurred. + /// The field path in the JSON file where the error occurred. + /// A human-readable description of the error. + public JsonValidatorErrorModel(int line, string path, string message) + { + this.Line = line; + this.Path = path; + this.Message = message; + } + } +} diff --git a/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorModel.cs b/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorModel.cs new file mode 100644 index 00000000..4c122d4f --- /dev/null +++ b/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorModel.cs @@ -0,0 +1,92 @@ +using System.Collections.Generic; +using System.Linq; + +namespace StardewModdingAPI.Web.ViewModels.JsonValidator +{ + /// The view model for the JSON validator page. + public class JsonValidatorModel + { + /********* + ** Accessors + *********/ + /// The root URL for the log parser controller. + public string SectionUrl { get; set; } + + /// The paste ID. + public string PasteID { get; set; } + + /// The schema name with which the JSON was validated. + public string SchemaName { get; set; } + + /// The supported JSON schemas (names indexed by ID). + public readonly IDictionary SchemaFormats; + + /// The validated content. + public string Content { get; set; } + + /// The schema validation errors, if any. + public JsonValidatorErrorModel[] Errors { get; set; } = new JsonValidatorErrorModel[0]; + + /// An error which occurred while uploading the JSON to Pastebin. + public string UploadError { get; set; } + + /// An error which occurred while parsing the JSON. + public string ParseError { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public JsonValidatorModel() { } + + /// Construct an instance. + /// The root URL for the log parser controller. + /// The paste ID. + /// The schema name with which the JSON was validated. + /// The supported JSON schemas (names indexed by ID). + public JsonValidatorModel(string sectionUrl, string pasteID, string schemaName, IDictionary schemaFormats) + { + this.SectionUrl = sectionUrl; + this.PasteID = pasteID; + this.SchemaName = schemaName; + this.SchemaFormats = schemaFormats; + } + + /// Set the validated content. + /// The validated content. + public JsonValidatorModel SetContent(string content) + { + this.Content = content; + + return this; + } + + /// Set the error which occurred while uploading the log to Pastebin. + /// The error message. + public JsonValidatorModel SetUploadError(string error) + { + this.UploadError = error; + + return this; + } + + /// Set the error which occurred while parsing the JSON. + /// The error message. + public JsonValidatorModel SetParseError(string error) + { + this.ParseError = error; + + return this; + } + + /// Add validation errors to the response. + /// The schema validation errors. + public JsonValidatorModel AddErrors(params JsonValidatorErrorModel[] errors) + { + this.Errors = this.Errors.Concat(errors).ToArray(); + + return this; + } + } +} diff --git a/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorRequestModel.cs b/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorRequestModel.cs new file mode 100644 index 00000000..c8e851bf --- /dev/null +++ b/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorRequestModel.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Web.ViewModels.JsonValidator +{ + /// The view model for a JSON validation request. + public class JsonValidatorRequestModel + { + /********* + ** Accessors + *********/ + /// The schema name with which to validate the JSON. + public string SchemaName { get; set; } + + /// The raw content to validate. + public string Content { get; set; } + } +} diff --git a/src/SMAPI.Web/Views/JsonValidator/Index.cshtml b/src/SMAPI.Web/Views/JsonValidator/Index.cshtml new file mode 100644 index 00000000..cd7ca912 --- /dev/null +++ b/src/SMAPI.Web/Views/JsonValidator/Index.cshtml @@ -0,0 +1,126 @@ +@using StardewModdingAPI.Web.ViewModels.JsonValidator +@model JsonValidatorModel + +@{ + ViewData["Title"] = "JSON validator"; +} + +@section Head { + @if (Model.PasteID != null) + { + + } + + + + + + + + + +} + +@* upload result banner *@ +@if (Model.UploadError != null) +{ + +} +else if (Model.ParseError != null) +{ + +} +else if (Model.PasteID != null) +{ + +} + +@* upload new file *@ +@if (Model.Content == null) +{ +

Upload a JSON file

+
+
    +
  1. + Choose the JSON format:
    + +
  2. +
  3. + Drag the file onto this textbox (or paste the text in):
    + +
  4. +
  5. + Click this button:
    + +
  6. +
+
+} + +@* validation results *@ +@if (Model.Content != null) +{ +
+ @if (Model.UploadError == null) + { +
+ Change JSON format: + +
+ +

Validation errors

+ @if (Model.Errors.Any()) + { + + + + + + + + @foreach (JsonValidatorErrorModel error in Model.Errors) + { + + + + + + } +
LineFieldError
@error.Line@error.Path@error.Message
+ } + else + { +

No errors found.

+ } + } + +

Raw content

+
@Model.Content
+
+} diff --git a/src/SMAPI.Web/Views/Shared/_Layout.cshtml b/src/SMAPI.Web/Views/Shared/_Layout.cshtml index 4c602b29..9911ef0e 100644 --- a/src/SMAPI.Web/Views/Shared/_Layout.cshtml +++ b/src/SMAPI.Web/Views/Shared/_Layout.cshtml @@ -16,9 +16,14 @@

SMAPI

+ +

Tools

+
diff --git a/src/SMAPI.Web/appsettings.Development.json b/src/SMAPI.Web/appsettings.Development.json index e6b4a1b1..baf7efb7 100644 --- a/src/SMAPI.Web/appsettings.Development.json +++ b/src/SMAPI.Web/appsettings.Development.json @@ -12,6 +12,7 @@ "RootUrl": "http://localhost:59482/", "ModListUrl": "http://localhost:59482/mods/", "LogParserUrl": "http://localhost:59482/log/", + "JsonValidatorUrl": "http://localhost:59482/json/", "BetaEnabled": false, "BetaBlurb": null }, diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json index f9777f87..a440cf42 100644 --- a/src/SMAPI.Web/appsettings.json +++ b/src/SMAPI.Web/appsettings.json @@ -19,6 +19,7 @@ "RootUrl": null, // see top note "ModListUrl": null, // see top note "LogParserUrl": null, // see top note + "JsonValidatorUrl": null, // see top note "BetaEnabled": null, // see top note "BetaBlurb": null // see top note }, diff --git a/src/SMAPI.Web/wwwroot/Content/css/json-validator.css b/src/SMAPI.Web/wwwroot/Content/css/json-validator.css new file mode 100644 index 00000000..f9aeb18b --- /dev/null +++ b/src/SMAPI.Web/wwwroot/Content/css/json-validator.css @@ -0,0 +1,98 @@ +/********* +** Main layout +*********/ +#content { + max-width: 100%; +} + +#output { + padding: 10px; + overflow: auto; +} + +#output table td { + font-family: monospace; +} + +#output table tr th, +#output table tr td { + padding: 0 0.75rem; + white-space: pre-wrap; +} + + +/********* +** Result banner +*********/ +.banner { + border: 2px solid gray; + border-radius: 5px; + margin-top: 1em; + padding: 1em; +} + +.banner.success { + border-color: green; + background: #CFC; +} + +.banner.error { + border-color: red; + background: #FCC; +} + +/********* +** Validation results +*********/ +.table { + border-bottom: 1px dashed #888888; + margin-bottom: 5px; +} + +#metadata th, #metadata td { + text-align: left; + padding-right: 0.7em; +} + +.table { + border: 1px solid #000000; + background: #ffffff; + border-radius: 5px; + border-spacing: 1px; + overflow: hidden; + cursor: default; + box-shadow: 1px 1px 1px 1px #dddddd; +} + +.table tr { + background: #eee; +} + +.table tr:nth-child(even) { + background: #fff; +} + +/********* +** Upload form +*********/ +#input { + width: 100%; + height: 20em; + max-height: 70%; + margin: auto; + box-sizing: border-box; + border-radius: 5px; + border: 1px solid #000088; + outline: none; + box-shadow: inset 0px 0px 1px 1px rgba(0, 0, 192, .2); +} + +#submit { + font-size: 1.5em; + border-radius: 5px; + outline: none; + box-shadow: inset 0 0 1px 1px rgba(0, 0, 0, .2); + cursor: pointer; + border: 1px solid #008800; + background-color: #cfc; +} diff --git a/src/SMAPI.Web/wwwroot/Content/css/main.css b/src/SMAPI.Web/wwwroot/Content/css/main.css index 57eeee88..dcc7a798 100644 --- a/src/SMAPI.Web/wwwroot/Content/css/main.css +++ b/src/SMAPI.Web/wwwroot/Content/css/main.css @@ -73,7 +73,7 @@ a { } #sidebar h4 { - margin: 0 0 0.2em 0; + margin: 1.5em 0 0.2em 0; width: 10em; border-bottom: 1px solid #CCC; font-size: 0.8em; diff --git a/src/SMAPI.Web/wwwroot/Content/js/json-validator.js b/src/SMAPI.Web/wwwroot/Content/js/json-validator.js new file mode 100644 index 00000000..3f7a1775 --- /dev/null +++ b/src/SMAPI.Web/wwwroot/Content/js/json-validator.js @@ -0,0 +1,60 @@ +/* globals $ */ + +var smapi = smapi || {}; +smapi.jsonValidator = function (sectionUrl, pasteID) { + /** + * Rebuild the syntax-highlighted element. + */ + var formatCode = function () { + Sunlight.highlightAll(); + }; + + /** + * Initialise the JSON validator page. + */ + var init = function () { + // code formatting + formatCode(); + + // change format + $("#output #format").on("change", function() { + var schemaName = $(this).val(); + location.href = new URL(schemaName + "/" + pasteID, sectionUrl).toString(); + }); + + // upload form + var input = $("#input"); + if (input.length) { + // disable submit if it's empty + var toggleSubmit = function () { + var hasText = !!input.val().trim(); + submit.prop("disabled", !hasText); + }; + input.on("input", toggleSubmit); + toggleSubmit(); + + // drag & drop file + input.on({ + 'dragover dragenter': function (e) { + e.preventDefault(); + e.stopPropagation(); + }, + 'drop': function (e) { + var dataTransfer = e.originalEvent.dataTransfer; + if (dataTransfer && dataTransfer.files.length) { + e.preventDefault(); + e.stopPropagation(); + var file = dataTransfer.files[0]; + var reader = new FileReader(); + reader.onload = $.proxy(function (file, $input, event) { + $input.val(event.target.result); + toggleSubmit(); + }, this, file, $("#input")); + reader.readAsText(file); + } + } + }); + } + }; + init(); +}; diff --git a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js index e87a1a5c..e6c7591c 100644 --- a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js +++ b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js @@ -23,7 +23,7 @@ smapi.logParser = function (data, sectionUrl) { } // set local time started - if(data) + if (data) data.localTimeStarted = ("0" + data.logStarted.getHours()).slice(-2) + ":" + ("0" + data.logStarted.getMinutes()).slice(-2); // init app @@ -100,7 +100,7 @@ smapi.logParser = function (data, sectionUrl) { updateModFilters(); }, - filtersAllow: function(modId, level) { + filtersAllow: function (modId, level) { return this.showMods[modId] !== false && this.showLevels[level] !== false; }, @@ -121,16 +121,15 @@ smapi.logParser = function (data, sectionUrl) { var submit = $("#submit"); // instruction OS chooser - var chooseSystem = function() { + var chooseSystem = function () { systemInstructions.hide(); systemInstructions.filter("[data-os='" + $("input[name='os']:checked").val() + "']").show(); - } + }; systemOptions.on("click", chooseSystem); chooseSystem(); // disable submit if it's empty - var toggleSubmit = function() - { + var toggleSubmit = function () { var hasText = !!input.val().trim(); submit.prop("disabled", !hasText); } @@ -139,18 +138,18 @@ smapi.logParser = function (data, sectionUrl) { // drag & drop file input.on({ - 'dragover dragenter': function(e) { + 'dragover dragenter': function (e) { e.preventDefault(); e.stopPropagation(); }, - 'drop': function(e) { + 'drop': function (e) { var dataTransfer = e.originalEvent.dataTransfer; if (dataTransfer && dataTransfer.files.length) { e.preventDefault(); e.stopPropagation(); var file = dataTransfer.files[0]; var reader = new FileReader(); - reader.onload = $.proxy(function(file, $input, event) { + reader.onload = $.proxy(function (file, $input, event) { $input.val(event.target.result); toggleSubmit(); }, this, file, $("#input")); diff --git a/src/SMAPI.Web/wwwroot/schemas/manifest.json b/src/SMAPI.Web/wwwroot/schemas/manifest.json new file mode 100644 index 00000000..06173333 --- /dev/null +++ b/src/SMAPI.Web/wwwroot/schemas/manifest.json @@ -0,0 +1,120 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://smapi.io/schemas/manifest.json", + "title": "SMAPI manifest", + "description": "Manifest file for a SMAPI mod or content pack", + "type": "object", + "properties": { + "Name": { + "title": "Mod name", + "description": "The mod's display name. SMAPI uses this in player messages, logs, and errors.", + "type": "string", + "examples": [ "Lookup Anything" ] + }, + "Author": { + "title": "Mod author", + "description": "The name of the person who created the mod. Ideally this should include the username used to publish mods.", + "type": "string", + "examples": [ "Pathoschild" ] + }, + "Version": { + "title": "Mod version", + "description": "The mod's semantic version. Make sure you update this for each release! SMAPI uses this for update checks, mod dependencies, and compatibility blacklists (if the mod breaks in a future version of the game).", + "$ref": "#/definitions/SemanticVersion" + }, + "Description": { + "title": "Mod description", + "description": "A short explanation of what your mod does (one or two sentences), shown in the SMAPI log.", + "type": "string", + "examples": [ "View metadata about anything by pressing a button." ] + }, + "UniqueID": { + "title": "Mod unique ID", + "description": "A unique identifier for your mod. The recommended format is \"Username.ModName\", with no spaces or special characters. SMAPI uses this for update checks, mod dependencies, and compatibility blacklists (if the mod breaks in a future version of the game). When another mod needs to reference this mod, it uses the unique ID.", + "$ref": "#/definitions/ModID" + }, + "EntryDll": { + "title": "Mod entry DLL", + "description": "The DLL filename SMAPI should load for this mod. Mutually exclusive with ContentPackFor.", + "type": "string", + "pattern": "^[a-zA-Z0-9_.-]+\\.dll$", + "examples": "LookupAnything.dll" + }, + "ContentPackFor": { + "title": "Content pack for", + "description": "Specifies the mod which can read this content pack.", + "type": "object", + "properties": { + "UniqueID": { + "title": "Required unique ID", + "description": "The unique ID of the mod which can read this content pack.", + "$ref": "#/definitions/ModID" + }, + "MinimumVersion": { + "title": "Required minimum version", + "description": "The minimum semantic version of the mod which can read this content pack, if applicable.", + "$ref": "#/definitions/SemanticVersion" + } + }, + + "required": [ "UniqueID" ] + }, + "Dependencies": { + "title": "Mod dependencies", + "description": "Specifies other mods to load before this mod. If a dependency is required and a player tries to use the mod without the dependency installed, the mod won't be loaded and they'll see a friendly message saying they need to install those.", + "type": "array", + "items": { + "type": "object", + "properties": { + "UniqueID": { + "title": "Dependency unique ID", + "description": "The unique ID of the mod to load first.", + "$ref": "#/definitions/ModID" + }, + "MinimumVersion": { + "title": "Dependency minimum version", + "description": "The minimum semantic version of the mod to load first, if applicable.", + "$ref": "#/definitions/SemanticVersion" + }, + "IsRequired": { + "title": "Dependency is required", + "description": "Whether the dependency is required. Default true if not specified." + } + }, + "required": [ "UniqueID" ] + } + }, + "UpdateKeys": { + "title": "Mod update keys", + "description": "Specifies where SMAPI should check for mod updates, so it can alert the user with a link to your mod page. See https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest#Update_checks for more info.", + "type": "array", + "items": { + "type": "string", + "pattern": "^(Chucklefish:\\d+|Nexus:\\d+|GitHub:[A-Za-z0-9_]+/[A-Za-z0-9_]+|ModDrop:\\d+)$" + } + } + }, + + "required": [ "Name", "Author", "Version", "Description", "UniqueID" ], + "oneOf": [ + { + "required": [ "EntryDll" ] + }, + { + "required": [ "ContentPackFor" ] + } + ], + + "definitions": { + "SemanticVersion": { + "type": "string", + "pattern": "(?>(?0|[1-9]\\d*))\\.(?>(?0|[1-9]\\d*))(?>(?:\\.(?0|[1-9]\\d*))?)(?:-(?(?>[a-zA-Z0-9]+[\\-\\.]?)+))?", // derived from SMAPI.Toolkit.SemanticVersion + "examples": [ "1.0.0", "1.0.1-beta.2" ] + }, + "ModID": { + "type": "string", + "pattern": "^[a-zA-Z0-9_.-]+$", // derived from SMAPI.Toolkit.Utilities.PathUtilities.IsSlug + "examples": [ "Pathoschild.LookupAnything" ] + } + } +} -- cgit From fd77ae93d59222d70c86aebfc044f3af11063372 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 9 Aug 2019 01:18:05 -0400 Subject: fix typos and inconsistent spelling --- .gitattributes | 2 +- docs/release-notes.md | 49 ++++---- src/SMAPI.Installer/InteractiveInstaller.cs | 12 +- src/SMAPI.Installer/Program.cs | 2 +- .../ConsoleWriting/ColorfulConsoleWriter.cs | 2 +- .../Mock/Netcode/NetFieldBase.cs | 2 +- .../NetFieldAnalyzer.cs | 8 +- .../ObsoleteFieldAnalyzer.cs | 2 +- .../Framework/ModFileManager.cs | 4 +- .../Framework/Commands/Player/AddCommand.cs | 2 +- .../Framework/Commands/Player/ListItemsCommand.cs | 4 +- .../Framework/Commands/World/SetTimeCommand.cs | 2 +- src/SMAPI.Tests/Core/ModResolverTests.cs | 12 +- src/SMAPI.Tests/Core/TranslationTests.cs | 2 +- src/SMAPI.Tests/Toolkit/PathUtilitiesTests.cs | 6 +- src/SMAPI.Tests/Utilities/SemanticVersionTests.cs | 8 +- .../ISemanticVersion.cs | 2 +- .../Clients/WebApi/ModExtendedMetadataModel.cs | 4 +- .../Framework/Clients/WebApi/ModSeachModel.cs | 2 +- .../Clients/WebApi/ModSearchEntryModel.cs | 2 +- .../Framework/Clients/WebApi/WebApiClient.cs | 2 +- .../Framework/GameScanning/GameScanner.cs | 2 +- .../Framework/ModData/ModDataModel.cs | 4 +- .../Framework/ModData/ModDataRecord.cs | 6 +- .../ModData/ModDataRecordVersionedFields.cs | 4 +- src/SMAPI.Toolkit/Framework/ModData/ModWarning.cs | 4 +- .../Framework/ModScanning/ModFolder.cs | 2 +- .../Framework/ModScanning/ModScanner.cs | 6 +- src/SMAPI.Toolkit/ModToolkit.cs | 2 +- src/SMAPI.Toolkit/SemanticVersion.cs | 24 ++-- .../Converters/ManifestContentPackForConverter.cs | 50 -------- .../Converters/ManifestDependencyArrayConverter.cs | 60 --------- .../Converters/SemanticVersionConverter.cs | 86 ------------- .../Converters/SimpleReadOnlyConverter.cs | 76 ------------ .../Serialisation/InternalExtensions.cs | 21 ---- src/SMAPI.Toolkit/Serialisation/JsonHelper.cs | 136 --------------------- src/SMAPI.Toolkit/Serialisation/Models/Manifest.cs | 74 ----------- .../Serialisation/Models/ManifestContentPackFor.cs | 15 --- .../Serialisation/Models/ManifestDependency.cs | 35 ------ src/SMAPI.Toolkit/Serialisation/SParseException.cs | 17 --- .../Converters/ManifestContentPackForConverter.cs | 50 ++++++++ .../Converters/ManifestDependencyArrayConverter.cs | 60 +++++++++ .../Converters/SemanticVersionConverter.cs | 86 +++++++++++++ .../Converters/SimpleReadOnlyConverter.cs | 76 ++++++++++++ .../Serialization/InternalExtensions.cs | 21 ++++ src/SMAPI.Toolkit/Serialization/JsonHelper.cs | 136 +++++++++++++++++++++ src/SMAPI.Toolkit/Serialization/Models/Manifest.cs | 74 +++++++++++ .../Serialization/Models/ManifestContentPackFor.cs | 15 +++ .../Serialization/Models/ManifestDependency.cs | 35 ++++++ src/SMAPI.Toolkit/Serialization/SParseException.cs | 17 +++ src/SMAPI.Toolkit/Utilities/PathUtilities.cs | 20 +-- src/SMAPI.Web/BackgroundService.cs | 6 +- .../Controllers/JsonValidatorController.cs | 14 +-- src/SMAPI.Web/Controllers/ModsApiController.cs | 2 +- .../Framework/AllowLargePostsAttribute.cs | 2 +- .../Framework/Caching/Mods/ModCacheRepository.cs | 10 +- .../Caching/UtcDateTimeOffsetSerializer.cs | 2 +- .../Framework/JobDashboardAuthorizationFilter.cs | 4 +- src/SMAPI.Web/Framework/LogParsing/LogParser.cs | 2 +- .../Framework/ModRepositories/BaseRepository.cs | 6 +- .../ModRepositories/ChucklefishRepository.cs | 2 +- .../Framework/ModRepositories/GitHubRepository.cs | 2 +- .../Framework/ModRepositories/NexusRepository.cs | 2 +- src/SMAPI.Web/Startup.cs | 2 +- src/SMAPI.Web/ViewModels/LogParserModel.cs | 2 +- src/SMAPI.Web/wwwroot/Content/js/json-validator.js | 2 +- src/SMAPI.Web/wwwroot/schemas/content-patcher.json | 6 +- src/SMAPI.sln.DotSettings | 42 +++++++ src/SMAPI/Context.cs | 4 +- src/SMAPI/Enums/LoadStage.cs | 10 +- src/SMAPI/Events/IGameLoopEvents.cs | 4 +- src/SMAPI/Events/IModEvents.cs | 4 +- src/SMAPI/Events/ISpecialisedEvents.cs | 4 +- src/SMAPI/Events/LoadStageChangedEventArgs.cs | 2 +- .../Events/UnvalidatedUpdateTickedEventArgs.cs | 2 +- .../Events/UnvalidatedUpdateTickingEventArgs.cs | 2 +- src/SMAPI/Framework/CommandManager.cs | 14 +-- src/SMAPI/Framework/Content/AssetData.cs | 10 +- .../Framework/Content/AssetDataForDictionary.cs | 10 +- src/SMAPI/Framework/Content/AssetDataForImage.cs | 10 +- src/SMAPI/Framework/Content/AssetDataForObject.cs | 20 +-- src/SMAPI/Framework/Content/AssetInfo.cs | 22 ++-- src/SMAPI/Framework/Content/ContentCache.cs | 30 ++--- src/SMAPI/Framework/ContentCoordinator.cs | 8 +- .../ContentManagers/BaseContentManager.cs | 34 +++--- .../ContentManagers/GameContentManager.cs | 58 ++++----- .../Framework/ContentManagers/IContentManager.cs | 10 +- .../Framework/ContentManagers/ModContentManager.cs | 28 ++--- src/SMAPI/Framework/ContentPack.cs | 10 +- src/SMAPI/Framework/Events/EventManager.cs | 18 +-- src/SMAPI/Framework/Events/ModEvents.cs | 6 +- src/SMAPI/Framework/Events/ModGameLoopEvents.cs | 2 +- src/SMAPI/Framework/Events/ModSpecialisedEvents.cs | 6 +- src/SMAPI/Framework/GameVersion.cs | 2 +- src/SMAPI/Framework/InternalExtensions.cs | 2 +- src/SMAPI/Framework/ModHelpers/ContentHelper.cs | 14 +-- .../Framework/ModHelpers/ContentPackHelper.cs | 4 +- src/SMAPI/Framework/ModHelpers/DataHelper.cs | 12 +- src/SMAPI/Framework/ModHelpers/ModHelper.cs | 4 +- .../Framework/ModHelpers/ModRegistryHelper.cs | 4 +- src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs | 2 +- src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 10 +- .../Framework/ModLoading/Finders/TypeFinder.cs | 2 +- .../ModLoading/InstructionHandleResult.cs | 4 +- .../Framework/ModLoading/ModDependencyStatus.cs | 4 +- src/SMAPI/Framework/ModLoading/ModResolver.cs | 8 +- .../Framework/ModLoading/TypeReferenceComparer.cs | 2 +- src/SMAPI/Framework/ModRegistry.cs | 6 +- src/SMAPI/Framework/Monitor.cs | 2 +- src/SMAPI/Framework/Networking/MessageType.cs | 2 +- src/SMAPI/Framework/Reflection/Reflector.cs | 2 +- src/SMAPI/Framework/SCore.cs | 56 ++++----- src/SMAPI/Framework/SGame.cs | 50 ++++---- src/SMAPI/Framework/SGameConstructorHack.cs | 4 +- src/SMAPI/Framework/SMultiplayer.cs | 20 +-- .../Framework/Serialisation/ColorConverter.cs | 47 ------- .../Framework/Serialisation/PointConverter.cs | 43 ------- .../Framework/Serialisation/RectangleConverter.cs | 52 -------- .../Framework/Serialization/ColorConverter.cs | 47 +++++++ .../Framework/Serialization/PointConverter.cs | 43 +++++++ .../Framework/Serialization/RectangleConverter.cs | 52 ++++++++ .../FieldWatchers/ComparableListWatcher.cs | 2 +- src/SMAPI/IAssetInfo.cs | 6 +- src/SMAPI/IContentHelper.cs | 4 +- src/SMAPI/IContentPack.cs | 2 +- src/SMAPI/IContentPackHelper.cs | 2 +- src/SMAPI/IDataHelper.cs | 2 +- src/SMAPI/Metadata/CoreAssetPropagator.cs | 42 +++---- src/SMAPI/Metadata/InstructionMetadata.cs | 10 +- src/SMAPI/Mod.cs | 2 +- src/SMAPI/Program.cs | 4 +- src/SMAPI/SemanticVersion.cs | 4 +- src/SMAPI/Translation.cs | 4 +- src/SMAPI/Utilities/SDate.cs | 6 +- 134 files changed, 1207 insertions(+), 1164 deletions(-) delete mode 100644 src/SMAPI.Toolkit/Serialisation/Converters/ManifestContentPackForConverter.cs delete mode 100644 src/SMAPI.Toolkit/Serialisation/Converters/ManifestDependencyArrayConverter.cs delete mode 100644 src/SMAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs delete mode 100644 src/SMAPI.Toolkit/Serialisation/Converters/SimpleReadOnlyConverter.cs delete mode 100644 src/SMAPI.Toolkit/Serialisation/InternalExtensions.cs delete mode 100644 src/SMAPI.Toolkit/Serialisation/JsonHelper.cs delete mode 100644 src/SMAPI.Toolkit/Serialisation/Models/Manifest.cs delete mode 100644 src/SMAPI.Toolkit/Serialisation/Models/ManifestContentPackFor.cs delete mode 100644 src/SMAPI.Toolkit/Serialisation/Models/ManifestDependency.cs delete mode 100644 src/SMAPI.Toolkit/Serialisation/SParseException.cs create mode 100644 src/SMAPI.Toolkit/Serialization/Converters/ManifestContentPackForConverter.cs create mode 100644 src/SMAPI.Toolkit/Serialization/Converters/ManifestDependencyArrayConverter.cs create mode 100644 src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs create mode 100644 src/SMAPI.Toolkit/Serialization/Converters/SimpleReadOnlyConverter.cs create mode 100644 src/SMAPI.Toolkit/Serialization/InternalExtensions.cs create mode 100644 src/SMAPI.Toolkit/Serialization/JsonHelper.cs create mode 100644 src/SMAPI.Toolkit/Serialization/Models/Manifest.cs create mode 100644 src/SMAPI.Toolkit/Serialization/Models/ManifestContentPackFor.cs create mode 100644 src/SMAPI.Toolkit/Serialization/Models/ManifestDependency.cs create mode 100644 src/SMAPI.Toolkit/Serialization/SParseException.cs delete mode 100644 src/SMAPI/Framework/Serialisation/ColorConverter.cs delete mode 100644 src/SMAPI/Framework/Serialisation/PointConverter.cs delete mode 100644 src/SMAPI/Framework/Serialisation/RectangleConverter.cs create mode 100644 src/SMAPI/Framework/Serialization/ColorConverter.cs create mode 100644 src/SMAPI/Framework/Serialization/PointConverter.cs create mode 100644 src/SMAPI/Framework/Serialization/RectangleConverter.cs (limited to 'src/SMAPI.Web/Framework') diff --git a/.gitattributes b/.gitattributes index 67d0626d..1161a204 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,3 @@ -# normalise line endings +# normalize line endings * text=auto README.txt text=crlf diff --git a/docs/release-notes.md b/docs/release-notes.md index e253c3c0..5f964cfd 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -29,6 +29,7 @@ These changes have not been released yet. * Fixed map reloads resetting tilesheet seasons. * Fixed map reloads not updating door warps. * Fixed outdoor tilesheets being seasonalised when added to an indoor location. + * Fixed typos and inconsistent spelling. * For the mod compatibility list: * Now loads faster (since data is fetched in a background service). @@ -43,7 +44,7 @@ These changes have not been released yet. * Added support for referencing a schema in a JSON Schema-compatible text editor. * For modders: - * Mods are now loaded much earlier in the game launch. This lets mods intercept any content asset, but the game is not fully initialised when `Entry` is called (use the `GameLaunched` event if you need to run code when the game is initialised). + * Mods are now loaded much earlier in the game launch. This lets mods intercept any content asset, but the game is not fully initialized when `Entry` is called (use the `GameLaunched` event if you need to run code when the game is initialized). * Added support for content pack translations. * Added fields and methods: `IContentPack.HasFile`, `Context.IsGameLaunched`, and `SemanticVersion.TryParse`. * Added separate `LogNetworkTraffic` option to make verbose logging less overwhelmingly verbose. @@ -53,7 +54,7 @@ These changes have not been released yet. * Trace logs when loading mods are now more clear. * Clarified update-check errors for mods with multiple update keys. * Fixed custom maps loaded from `.xnb` files not having their tilesheet paths automatically adjusted. - * Fixed custom maps loaded from the mod folder with tilesheets in a subfolder not working crossplatform. All tilesheet paths are now normalised for the OS automatically. + * Fixed custom maps loaded from the mod folder with tilesheets in a subfolder not working crossplatform. All tilesheet paths are now normalized for the OS automatically. * Removed all deprecated APIs. * Removed the `Monitor.ExitGameImmediately` method. * Updated dependencies (including Json.NET 11.0.2 → 12.0.2, Mono.Cecil 0.10.1 → 0.10.4). @@ -73,7 +74,7 @@ Released 13 September 2019 for Stardew Valley 1.3.36. * For the web UI: * When filtering the mod list, clicking a mod link now automatically adds it to the visible mods. * Added log parser instructions for Android. - * Fixed log parser failing in some cases due to time format localisation. + * Fixed log parser failing in some cases due to time format localization. * For modders: * `this.Monitor.Log` now defaults to the `Trace` log level instead of `Debug`. The change will only take effect when you recompile the mod. @@ -141,12 +142,12 @@ Released 09 January 2019 for Stardew Valley 1.3.32–33. * Added locale to context trace logs. * Fixed error loading custom map tilesheets in some cases. * Fixed error when swapping maps mid-session for a location with interior doors. - * Fixed `Constants.SaveFolderName` and `CurrentSavePath` not available during early load stages when using `Specialised.LoadStageChanged` event. + * Fixed `Constants.SaveFolderName` and `CurrentSavePath` not available during early load stages when using `Specialized.LoadStageChanged` event. * Fixed `LoadStage.SaveParsed` raised before the parsed save data is available. * Fixed 'unknown mod' deprecation warnings showing the wrong stack trace. * Fixed `e.Cursor` in input events showing wrong grab tile when player using a controller moves without moving the viewpoint. - * Fixed incorrect 'bypassed safety checks' warning for mods using the new `Specialised.LoadStageChanged` event in 2.10. - * Deprecated `EntryDll` values whose capitalisation don't match the actual file. (This works on Windows, but causes errors for Linux/Mac players.) + * Fixed incorrect 'bypassed safety checks' warning for mods using the new `Specialized.LoadStageChanged` event in 2.10. + * Deprecated `EntryDll` values whose capitalization don't match the actual file. (This works on Windows, but causes errors for Linux/Mac players.) ## 2.10.1 Released 30 December 2018 for Stardew Valley 1.3.32–33. @@ -163,9 +164,9 @@ Released 29 December 2018 for Stardew Valley 1.3.32–33. * Tweaked installer to reduce antivirus false positives. * For modders: - * Added [events](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Events): `GameLoop.OneSecondUpdateTicking`, `GameLoop.OneSecondUpdateTicked`, and `Specialised.LoadStageChanged`. + * Added [events](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Events): `GameLoop.OneSecondUpdateTicking`, `GameLoop.OneSecondUpdateTicked`, and `Specialized.LoadStageChanged`. * Added `e.IsCurrentLocation` event arg to `World` events. - * You can now use `helper.Data.Read/WriteSaveData` as soon as the save is loaded (instead of once the world is initialised). + * You can now use `helper.Data.Read/WriteSaveData` as soon as the save is loaded (instead of once the world is initialized). * Increased deprecation levels to _info_ for the upcoming SMAPI 3.0. * For the web UI: @@ -338,7 +339,7 @@ Released 14 August 2018 for Stardew Valley 1.3.28. * dialogue; * map tilesheets. * Added `--mods-path` CLI command-line argument to switch between mod folders. - * All enums are now JSON-serialised by name instead of numeric value. (Previously only a few enums were serialised that way. JSON files which already have numeric enum values will still be parsed fine.) + * All enums are now JSON-serialized by name instead of numeric value. (Previously only a few enums were serialized that way. JSON files which already have numeric enum values will still be parsed fine.) * Fixed false compatibility error when constructing multidimensional arrays. * Fixed `.ToSButton()` methods not being public. @@ -365,7 +366,7 @@ Released 01 August 2018 for Stardew Valley 1.3.27. * Improved the Console Commands mod: * Added `player_add name`, which adds items to your inventory by name instead of ID. * Fixed `world_setseason` not running season-change logic. - * Fixed `world_setseason` not normalising the season value. + * Fixed `world_setseason` not normalizing the season value. * Fixed `world_settime` sometimes breaking NPC schedules (e.g. so they stay in bed). * Removed the `player_setlevel` and `player_setspeed` commands, which weren't implemented in a useful way. Use a mod like CJB Cheats Menu if you need those. * Fixed `SEHException` errors for some players. @@ -456,7 +457,7 @@ Released 11 April 2018 for Stardew Valley 1.2.30–1.2.33. * Fixed mod update alerts not shown if one mod has an invalid remote version. * Fixed SMAPI update alerts linking to the GitHub repository instead of [smapi.io](https://smapi.io). * Fixed SMAPI update alerts for draft releases. - * Fixed error when two content packs use different capitalisation for the same required mod ID. + * Fixed error when two content packs use different capitalization for the same required mod ID. * Fixed rare crash if the game duplicates an item. * For the [log parser](https://log.smapi.io): @@ -531,8 +532,8 @@ Released 24 February 2018 for Stardew Valley 1.2.30–1.2.33. * For modders: * Added support for content packs and new APIs to read them. * Added support for `ISemanticVersion` in JSON models. - * Added `SpecialisedEvents.UnvalidatedUpdateTick` event for specialised use cases. - * Added path normalising to `ReadJsonFile` and `WriteJsonFile` helpers (so no longer need `Path.Combine` with those). + * Added `SpecializedEvents.UnvalidatedUpdateTick` event for specialized use cases. + * Added path normalizing to `ReadJsonFile` and `WriteJsonFile` helpers (so no longer need `Path.Combine` with those). * Fixed deadlock in rare cases with asset loaders. * Fixed unhelpful error when a mod exposes a non-public API. * Fixed unhelpful error when a translation file has duplicate keys due to case-insensitivity. @@ -585,11 +586,11 @@ Released 26 December 2017 for Stardew Valley 1.2.30–1.2.33. * For modders: * **Added mod-provided APIs** to allow simple integrations between mods, even without direct assembly references. - * Added `GameEvents.FirstUpdateTick` event (called once after all mods are initialised). + * Added `GameEvents.FirstUpdateTick` event (called once after all mods are initialized). * Added `IsSuppressed` to input events so mods can optionally avoid handling keys another mod has already handled. * Added trace message for mods with no update keys. * Adjusted reflection API to match actual usage (e.g. renamed `GetPrivate*` to `Get*`), and deprecated previous methods. - * Fixed `GraphicsEvents.OnPostRenderEvent` not being raised in some specialised cases. + * Fixed `GraphicsEvents.OnPostRenderEvent` not being raised in some specialized cases. * Fixed reflection API error for properties missing a `get` and `set`. * Fixed issue where a mod could change the cursor position reported to other mods. * Updated compatibility list. @@ -614,7 +615,7 @@ Released 02 December 2017 for Stardew Valley 1.2.30–1.2.33. * Slightly improved the UI. * For modders: - * Added `helper.Content.NormaliseAssetName` method. + * Added `helper.Content.NormalizeAssetName` method. * Added `SDate.DaysSinceStart` property. * Fixed input events' `e.SuppressButton(button)` method ignoring specified button. * Fixed input events' `e.SuppressButton()` method not working with mouse buttons. @@ -769,7 +770,7 @@ For mod developers: * Added content helper properties for the game's current language. * Fixed `Context.IsPlayerFree` being false if the player is performing an action. * Fixed `GraphicsEvents.Resize` being raised before the game updates its window data. -* Fixed `SemanticVersion` not being deserialisable through Json.NET. +* Fixed `SemanticVersion` not being deserializable through Json.NET. * Fixed terminal not launching on Xfce Linux. For SMAPI developers: @@ -840,7 +841,7 @@ For modders: * SMAPI now automatically fixes tilesheet references for maps loaded from the mod folder. _When loading a map from the mod folder, SMAPI will automatically use tilesheets relative to the map file if they exists. Otherwise it will default to tilesheets in the game content._ * Added `Context.IsPlayerFree` for mods that need to check if the player can act (i.e. save is loaded, no menu is displayed, no cutscene is in progress, etc). -* Added `Context.IsInDrawLoop` for specialised mods. +* Added `Context.IsInDrawLoop` for specialized mods. * Fixed `smapi-crash.txt` being copied from the default log even if a different path is specified with `--log-path`. * Fixed the content API not matching XNB filenames with two dots (like `a.b.xnb`) if you don't specify the `.xnb` extension. * Fixed `debug` command output not printed to console. @@ -867,7 +868,7 @@ For players: For mod developers: * Added a `Context.IsWorldReady` flag for mods to use. - _This indicates whether a save is loaded and the world is finished initialising, which starts at the same point that `SaveEvents.AfterLoad` and `TimeEvents.AfterDayStarted` are raised. This is mainly useful for events which can be raised before the world is loaded (like update tick)._ + _This indicates whether a save is loaded and the world is finished initializing, which starts at the same point that `SaveEvents.AfterLoad` and `TimeEvents.AfterDayStarted` are raised. This is mainly useful for events which can be raised before the world is loaded (like update tick)._ * Added a `debug` console command which lets you run the game's debug commands (e.g. `debug warp FarmHouse 1 1` warps you to the farmhouse). * Added basic context info to logs to simplify troubleshooting. * Added a `Mod.Dispose` method which can be overriden to clean up before exit. This method isn't guaranteed to be called on every exit. @@ -905,8 +906,8 @@ For players: For mod developers: * Added a content API which loads custom textures/maps/data from the mod's folder (`.xnb` or `.png` format) or game content. * `Console.Out` messages are now written to the log file. -* `Monitor.ExitGameImmediately` now aborts SMAPI initialisation and events more quickly. -* Fixed value-changed events being raised when the player loads a save due to values being initialised. +* `Monitor.ExitGameImmediately` now aborts SMAPI initialization and events more quickly. +* Fixed value-changed events being raised when the player loads a save due to values being initialized. ## 1.10 Released 24 April 2017 for Stardew Valley 1.2.26. @@ -922,7 +923,7 @@ For players: * Replaced `player_addmelee` with `player_addweapon` with support for non-melee weapons. For mod developers: -* Mods are now initialised after the `Initialize`/`LoadContent` phase, which means the `GameEvents.Initialize` and `GameEvents.LoadContent` events are deprecated. You can move any logic in those methods to your mod's `Entry` method. +* Mods are now initialized after the `Initialize`/`LoadContent` phase, which means the `GameEvents.Initialize` and `GameEvents.LoadContent` events are deprecated. You can move any logic in those methods to your mod's `Entry` method. * Added `IsBetween` and string overloads to the `ISemanticVersion` methods. * Fixed mouse-changed event never updating prior mouse position. * Fixed `monitor.ExitGameImmediately` not working correctly. @@ -961,7 +962,7 @@ For mod developers: * The SMAPI log now has a simpler filename. * The SMAPI log now shows the OS caption (like "Windows 10") instead of its internal version when available. * The SMAPI log now always uses `\r\n` line endings to simplify crossplatform viewing. -* Fixed `SaveEvents.AfterLoad` being raised during the new-game intro before the player is initialised. +* Fixed `SaveEvents.AfterLoad` being raised during the new-game intro before the player is initialized. * Fixed SMAPI not recognising `Mod` instances that don't subclass `Mod` directly. * Several obsolete APIs have been removed (see [migration guides](https://stardewvalleywiki.com/Modding:Index#Migration_guides)), and all _notice_-level deprecations have been increased to _info_. @@ -1006,7 +1007,7 @@ For mod developers: * Added a mod registry which provides metadata about loaded mods. * The `Entry(…)` method is now deferred until all mods are loaded. * Fixed `SaveEvents.BeforeSave` and `.AfterSave` not triggering on days when the player shipped something. -* Fixed `PlayerEvents.LoadedGame` and `SaveEvents.AfterLoad` being fired before the world finishes initialising. +* Fixed `PlayerEvents.LoadedGame` and `SaveEvents.AfterLoad` being fired before the world finishes initializing. * Fixed some `LocationEvents`, `PlayerEvents`, and `TimeEvents` being fired during game startup. * Increased deprecation levels for `SObject`, `LogWriter` (not `Log`), and `Mod.Entry(ModHelper)` (not `Mod.Entry(IModHelper)`) to _pending removal_. Increased deprecation levels for `Mod.PerSaveConfigFolder`, `Mod.PerSaveConfigPath`, and `Version.VersionString` to _info_. diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index 41400617..4d313a3b 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -106,7 +106,7 @@ namespace StardewModdingApi.Installer /// Run the install or uninstall script. /// The command line arguments. /// - /// Initialisation flow: + /// Initialization flow: /// 1. Collect information (mainly OS and install path) and validate it. /// 2. Ask the user whether to install or uninstall. /// @@ -218,7 +218,7 @@ namespace StardewModdingApi.Installer ****/ // get theme writers var lightBackgroundWriter = new ColorfulConsoleWriter(EnvironmentUtility.DetectPlatform(), MonitorColorScheme.LightBackground); - var darkDarkgroundWriter = new ColorfulConsoleWriter(EnvironmentUtility.DetectPlatform(), MonitorColorScheme.DarkBackground); + var darkBackgroundWriter = new ColorfulConsoleWriter(EnvironmentUtility.DetectPlatform(), MonitorColorScheme.DarkBackground); // print question this.PrintPlain("Which text looks more readable?"); @@ -226,7 +226,7 @@ namespace StardewModdingApi.Installer Console.Write(" [1] "); lightBackgroundWriter.WriteLine("Dark text on light background", ConsoleLogLevel.Info); Console.Write(" [2] "); - darkDarkgroundWriter.WriteLine("Light text on dark background", ConsoleLogLevel.Info); + darkBackgroundWriter.WriteLine("Light text on dark background", ConsoleLogLevel.Info); Console.WriteLine(); // handle choice @@ -239,7 +239,7 @@ namespace StardewModdingApi.Installer break; case "2": scheme = MonitorColorScheme.DarkBackground; - this.ConsoleWriter = darkDarkgroundWriter; + this.ConsoleWriter = darkBackgroundWriter; break; default: throw new InvalidOperationException($"Unexpected action key '{choice}'."); @@ -646,7 +646,7 @@ namespace StardewModdingApi.Installer /// Delete a file or folder regardless of file permissions, and block until deletion completes. /// The file or folder to reset. - /// This method is mirred from FileUtilities.ForceDelete in the toolkit. + /// This method is mirrored from FileUtilities.ForceDelete in the toolkit. private void ForceDelete(FileSystemInfo entry) { // ignore if already deleted @@ -762,7 +762,7 @@ namespace StardewModdingApi.Installer continue; } - // normalise path + // normalize path if (platform == Platform.Windows) path = path.Replace("\"", ""); // in Windows, quotes are used to escape spaces and aren't part of the file path if (platform == Platform.Linux || platform == Platform.Mac) diff --git a/src/SMAPI.Installer/Program.cs b/src/SMAPI.Installer/Program.cs index 3c4d8593..b7fa45f5 100644 --- a/src/SMAPI.Installer/Program.cs +++ b/src/SMAPI.Installer/Program.cs @@ -36,7 +36,7 @@ namespace StardewModdingApi.Installer FileInfo zipFile = new FileInfo(Path.Combine(Program.InstallerPath, $"{(platform == PlatformID.Win32NT ? "windows" : "unix")}-install.dat")); if (!zipFile.Exists) { - Console.WriteLine($"Oops! Some of the installer files are missing; try redownloading the installer. (Missing file: {zipFile.FullName})"); + Console.WriteLine($"Oops! Some of the installer files are missing; try re-downloading the installer. (Missing file: {zipFile.FullName})"); Console.ReadLine(); return; } diff --git a/src/SMAPI.Internal/ConsoleWriting/ColorfulConsoleWriter.cs b/src/SMAPI.Internal/ConsoleWriting/ColorfulConsoleWriter.cs index 40c2d986..db016bae 100644 --- a/src/SMAPI.Internal/ConsoleWriting/ColorfulConsoleWriter.cs +++ b/src/SMAPI.Internal/ConsoleWriting/ColorfulConsoleWriter.cs @@ -126,7 +126,7 @@ namespace StardewModdingAPI.Internal.ConsoleWriting case ConsoleColor.Black: case ConsoleColor.Blue: case ConsoleColor.DarkBlue: - case ConsoleColor.DarkMagenta: // Powershell + case ConsoleColor.DarkMagenta: // PowerShell case ConsoleColor.DarkRed: case ConsoleColor.Red: return true; diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/Netcode/NetFieldBase.cs b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/Netcode/NetFieldBase.cs index 1684229a..140c6f59 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/Netcode/NetFieldBase.cs +++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/Netcode/NetFieldBase.cs @@ -2,7 +2,7 @@ namespace Netcode { /// A simplified version of Stardew Valley's Netcode.NetFieldBase for unit testing. - /// The type of the synchronised value. + /// The type of the synchronized value. /// The type of the current instance. public class NetFieldBase where TSelf : NetFieldBase { diff --git a/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs b/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs index 70c01418..a9b981bd 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs +++ b/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs @@ -199,7 +199,7 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer /********* ** Private methods *********/ - /// Analyse a member access syntax node and add a diagnostic message if applicable. + /// Analyze a member access syntax node and add a diagnostic message if applicable. /// The analysis context. /// Returns whether any warnings were added. private void AnalyzeMemberAccess(SyntaxNodeAnalysisContext context) @@ -231,7 +231,7 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer }); } - /// Analyse an explicit cast or 'x as y' node and add a diagnostic message if applicable. + /// Analyze an explicit cast or 'x as y' node and add a diagnostic message if applicable. /// The analysis context. /// Returns whether any warnings were added. private void AnalyzeCast(SyntaxNodeAnalysisContext context) @@ -248,7 +248,7 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer }); } - /// Analyse a binary comparison syntax node and add a diagnostic message if applicable. + /// Analyze a binary comparison syntax node and add a diagnostic message if applicable. /// The analysis context. /// Returns whether any warnings were added. private void AnalyzeBinaryComparison(SyntaxNodeAnalysisContext context) @@ -288,7 +288,7 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer } /// Handle exceptions raised while analyzing a node. - /// The node being analysed. + /// The node being analyzed. /// The callback to invoke. private void HandleErrors(SyntaxNode node, Action action) { diff --git a/src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs b/src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs index 6b935caa..d071f0c1 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs +++ b/src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs @@ -67,7 +67,7 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer /********* ** Private methods *********/ - /// Analyse a syntax node and add a diagnostic message if it references an obsolete field. + /// Analyze a syntax node and add a diagnostic message if it references an obsolete field. /// The analysis context. private void AnalyzeObsoleteFields(SyntaxNodeAnalysisContext context) { diff --git a/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs b/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs index e67a18c1..a852f133 100644 --- a/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs +++ b/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs @@ -3,8 +3,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; -using StardewModdingAPI.Toolkit.Serialisation; -using StardewModdingAPI.Toolkit.Serialisation.Models; +using StardewModdingAPI.Toolkit.Serialization; +using StardewModdingAPI.Toolkit.Serialization.Models; using StardewModdingAPI.Toolkit.Utilities; namespace StardewModdingAPI.ModBuildConfig.Framework diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs index 263e126c..6cb2b624 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs @@ -15,7 +15,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player /// Provides methods for searching and constructing items. private readonly ItemRepository Items = new ItemRepository(); - /// The type names recognised by this command. + /// The type names recognized by this command. private readonly string[] ValidTypes = Enum.GetNames(typeof(ItemType)).Concat(new[] { "Name" }).ToArray(); diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemsCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemsCommand.cs index 5b52e9a2..4232ce16 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemsCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemsCommand.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData; @@ -58,7 +58,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player /// The search string to find. private IEnumerable GetItems(string[] searchWords) { - // normalise search term + // normalize search term searchWords = searchWords?.Where(word => !string.IsNullOrWhiteSpace(word)).ToArray(); if (searchWords?.Any() == false) searchWords = null; diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetTimeCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetTimeCommand.cs index a6075013..9eae6741 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetTimeCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetTimeCommand.cs @@ -60,7 +60,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World { for (int i = 0; i > intervals; i--) { - Game1.timeOfDay = FromTimeSpan(ToTimeSpan(Game1.timeOfDay).Subtract(TimeSpan.FromMinutes(20))); // offset 20 mins so game updates to next interval + Game1.timeOfDay = FromTimeSpan(ToTimeSpan(Game1.timeOfDay).Subtract(TimeSpan.FromMinutes(20))); // offset 20 minutes so game updates to next interval Game1.performTenMinuteClockUpdate(); } } diff --git a/src/SMAPI.Tests/Core/ModResolverTests.cs b/src/SMAPI.Tests/Core/ModResolverTests.cs index ee1c0b99..c1aa0212 100644 --- a/src/SMAPI.Tests/Core/ModResolverTests.cs +++ b/src/SMAPI.Tests/Core/ModResolverTests.cs @@ -9,7 +9,7 @@ using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.ModData; -using StardewModdingAPI.Toolkit.Serialisation.Models; +using StardewModdingAPI.Toolkit.Serialization.Models; namespace StardewModdingAPI.Tests.Core { @@ -55,7 +55,7 @@ namespace StardewModdingAPI.Tests.Core Assert.IsNotNull(mod.Error, "The mod metadata did not have an error message set."); } - [Test(Description = "Assert that the resolver correctly reads manifest data from a randomised file.")] + [Test(Description = "Assert that the resolver correctly reads manifest data from a randomized file.")] public void ReadBasicManifest_CanReadFile() { // create manifest data @@ -468,7 +468,7 @@ namespace StardewModdingAPI.Tests.Core return Path.Combine(Path.GetTempPath(), "smapi-unit-tests", Guid.NewGuid().ToString("N")); } - /// Get a randomised basic manifest. + /// Get a randomized basic manifest. /// The value, or null for a generated value. /// The value, or null for a generated value. /// The value, or null for a generated value. @@ -492,14 +492,14 @@ namespace StardewModdingAPI.Tests.Core }; } - /// Get a randomised basic manifest. + /// Get a randomized basic manifest. /// The mod's name and unique ID. private Mock GetMetadata(string uniqueID) { return this.GetMetadata(this.GetManifest(uniqueID, "1.0")); } - /// Get a randomised basic manifest. + /// Get a randomized basic manifest. /// The mod's name and unique ID. /// The dependencies this mod requires. /// Whether the code being tested is allowed to change the mod status. @@ -509,7 +509,7 @@ namespace StardewModdingAPI.Tests.Core return this.GetMetadata(manifest, allowStatusChange); } - /// Get a randomised basic manifest. + /// Get a randomized basic manifest. /// The mod manifest. /// Whether the code being tested is allowed to change the mod status. private Mock GetMetadata(IManifest manifest, bool allowStatusChange = false) diff --git a/src/SMAPI.Tests/Core/TranslationTests.cs b/src/SMAPI.Tests/Core/TranslationTests.cs index 63404a41..eea301ae 100644 --- a/src/SMAPI.Tests/Core/TranslationTests.cs +++ b/src/SMAPI.Tests/Core/TranslationTests.cs @@ -245,7 +245,7 @@ namespace StardewModdingAPI.Tests.Core [TestCase("{{value}}", "value")] [TestCase("{{VaLuE}}", "vAlUe")] [TestCase("{{VaLuE }}", " vAlUe")] - public void Translation_Tokens_KeysAreNormalised(string text, string key) + public void Translation_Tokens_KeysAreNormalized(string text, string key) { // arrange string value = Guid.NewGuid().ToString("N"); diff --git a/src/SMAPI.Tests/Toolkit/PathUtilitiesTests.cs b/src/SMAPI.Tests/Toolkit/PathUtilitiesTests.cs index 229b9a14..3dc65ed5 100644 --- a/src/SMAPI.Tests/Toolkit/PathUtilitiesTests.cs +++ b/src/SMAPI.Tests/Toolkit/PathUtilitiesTests.cs @@ -25,7 +25,7 @@ namespace StardewModdingAPI.Tests.Toolkit return string.Join("|", PathUtilities.GetSegments(path)); } - [Test(Description = "Assert that NormalisePathSeparators returns the expected values.")] + [Test(Description = "Assert that NormalizePathSeparators returns the expected values.")] #if SMAPI_FOR_WINDOWS [TestCase("", ExpectedResult = "")] [TestCase("/", ExpectedResult = "")] @@ -47,9 +47,9 @@ namespace StardewModdingAPI.Tests.Toolkit [TestCase("C:/boop", ExpectedResult = "C:/boop")] [TestCase(@"C:\usr\bin//.././boop.exe", ExpectedResult = "C:/usr/bin/.././boop.exe")] #endif - public string NormalisePathSeparators(string path) + public string NormalizePathSeparators(string path) { - return PathUtilities.NormalisePathSeparators(path); + return PathUtilities.NormalizePathSeparators(path); } [Test(Description = "Assert that GetRelativePath returns the expected values.")] diff --git a/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs b/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs index 2e7719eb..8f64a5fb 100644 --- a/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs +++ b/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs @@ -243,19 +243,19 @@ namespace StardewModdingAPI.Tests.Utilities } /**** - ** Serialisable + ** Serializable ****/ [Test(Description = "Assert that SemanticVersion can be round-tripped through JSON with no special configuration.")] [TestCase("1.0")] - public void Serialisable(string versionStr) + public void Serializable(string versionStr) { // act string json = JsonConvert.SerializeObject(new SemanticVersion(versionStr)); SemanticVersion after = JsonConvert.DeserializeObject(json); // assert - Assert.IsNotNull(after, "The semantic version after deserialisation is unexpectedly null."); - Assert.AreEqual(versionStr, after.ToString(), "The semantic version after deserialisation doesn't match the input version."); + Assert.IsNotNull(after, "The semantic version after deserialization is unexpectedly null."); + Assert.AreEqual(versionStr, after.ToString(), "The semantic version after deserialization doesn't match the input version."); } /**** diff --git a/src/SMAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs b/src/SMAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs index 4a61f9ae..4ec87d7c 100644 --- a/src/SMAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs +++ b/src/SMAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs @@ -24,7 +24,7 @@ namespace StardewModdingAPI /********* ** Accessors *********/ - /// Whether this is a pre-release version. + /// Whether this is a prerelease version. bool IsPrerelease(); /// Get whether this version is older than the specified version. diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs index 989c18b0..bd5f71a9 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs @@ -48,7 +48,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi [JsonConverter(typeof(StringEnumConverter))] public WikiCompatibilityStatus? CompatibilityStatus { get; set; } - /// The human-readable summary of the compatibility status or workaround, without HTML formatitng. + /// The human-readable summary of the compatibility status or workaround, without HTML formatting. public string CompatibilitySummary { get; set; } /// The game or SMAPI version which broke this mod, if applicable. @@ -62,7 +62,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi [JsonConverter(typeof(StringEnumConverter))] public WikiCompatibilityStatus? BetaCompatibilityStatus { get; set; } - /// The human-readable summary of the compatibility status or workaround for the Stardew Valley beta (if any), without HTML formatitng. + /// The human-readable summary of the compatibility status or workaround for the Stardew Valley beta (if any), without HTML formatting. public string BetaCompatibilitySummary { get; set; } /// The beta game or SMAPI version which broke this mod, if applicable. diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs index e352e1cc..a2eaad16 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs @@ -21,7 +21,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// Construct an empty instance. public ModSearchModel() { - // needed for JSON deserialising + // needed for JSON deserializing } /// Construct an instance. diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs index bca47647..886cd5a1 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs @@ -19,7 +19,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// Construct an empty instance. public ModSearchEntryModel() { - // needed for JSON deserialising + // needed for JSON deserializing } /// Construct an instance. diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs index 7c3df384..80c8f62b 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; using Newtonsoft.Json; -using StardewModdingAPI.Toolkit.Serialisation; +using StardewModdingAPI.Toolkit.Serialization; namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi { diff --git a/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs b/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs index 60c7a682..212c70ef 100644 --- a/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs +++ b/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs @@ -29,7 +29,7 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning IEnumerable paths = this .GetCustomInstallPaths(platform) .Concat(this.GetDefaultInstallPaths(platform)) - .Select(PathUtilities.NormalisePathSeparators) + .Select(PathUtilities.NormalizePathSeparators) .Distinct(StringComparer.InvariantCultureIgnoreCase); // yield valid folders diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDataModel.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDataModel.cs index 18039762..dd0bd07b 100644 --- a/src/SMAPI.Toolkit/Framework/ModData/ModDataModel.cs +++ b/src/SMAPI.Toolkit/Framework/ModData/ModDataModel.cs @@ -112,8 +112,8 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData /********* ** Private methods *********/ - /// The method invoked after JSON deserialisation. - /// The deserialisation context. + /// The method invoked after JSON deserialization. + /// The deserialization context. [OnDeserialized] private void OnDeserialized(StreamingContext context) { diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs index 794ad2e4..f01ada7c 100644 --- a/src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs +++ b/src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs @@ -68,7 +68,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData } /// Get a semantic local version for update checks. - /// The remote version to normalise. + /// The remote version to normalize. public ISemanticVersion GetLocalVersionForUpdateChecks(ISemanticVersion version) { return this.MapLocalVersions != null && this.MapLocalVersions.TryGetValue(version.ToString(), out string newVersion) @@ -77,10 +77,10 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData } /// Get a semantic remote version for update checks. - /// The remote version to normalise. + /// The remote version to normalize. public string GetRemoteVersionForUpdateChecks(string version) { - // normalise version if possible + // normalize version if possible if (SemanticVersion.TryParse(version, out ISemanticVersion parsed)) version = parsed.ToString(); diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs index 237f2c66..9e22990d 100644 --- a/src/SMAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs +++ b/src/SMAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs @@ -32,14 +32,14 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData ** Public methods *********/ /// Get a semantic local version for update checks. - /// The remote version to normalise. + /// The remote version to normalize. public ISemanticVersion GetLocalVersionForUpdateChecks(ISemanticVersion version) { return this.DataRecord.GetLocalVersionForUpdateChecks(version); } /// Get a semantic remote version for update checks. - /// The remote version to normalise. + /// The remote version to normalize. public ISemanticVersion GetRemoteVersionForUpdateChecks(ISemanticVersion version) { if (version == null) diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModWarning.cs b/src/SMAPI.Toolkit/Framework/ModData/ModWarning.cs index d61c427f..e67616d0 100644 --- a/src/SMAPI.Toolkit/Framework/ModData/ModWarning.cs +++ b/src/SMAPI.Toolkit/Framework/ModData/ModWarning.cs @@ -13,7 +13,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData BrokenCodeLoaded = 1, /// The mod affects the save serializer in a way that may make saves unloadable without the mod. - ChangesSaveSerialiser = 2, + ChangesSaveSerializer = 2, /// The mod patches the game in a way that may impact stability. PatchesGame = 4, @@ -21,7 +21,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData /// The mod uses the dynamic keyword which won't work on Linux/Mac. UsesDynamic = 8, - /// The mod references specialised 'unvalided update tick' events which may impact stability. + /// The mod references specialized 'unvalidated update tick' events which may impact stability. UsesUnvalidatedUpdateTick = 16, /// The mod has no update keys set. diff --git a/src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs b/src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs index 4ce17c66..d0df09a1 100644 --- a/src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs +++ b/src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using StardewModdingAPI.Toolkit.Serialisation.Models; +using StardewModdingAPI.Toolkit.Serialization.Models; using StardewModdingAPI.Toolkit.Utilities; namespace StardewModdingAPI.Toolkit.Framework.ModScanning diff --git a/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs index 54cb2b8b..f11cc1a7 100644 --- a/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs +++ b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs @@ -3,8 +3,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; -using StardewModdingAPI.Toolkit.Serialisation; -using StardewModdingAPI.Toolkit.Serialisation.Models; +using StardewModdingAPI.Toolkit.Serialization; +using StardewModdingAPI.Toolkit.Serialization.Models; namespace StardewModdingAPI.Toolkit.Framework.ModScanning { @@ -118,7 +118,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning } } - // normalise display fields + // normalize display fields if (manifest != null) { manifest.Name = this.StripNewlines(manifest.Name); diff --git a/src/SMAPI.Toolkit/ModToolkit.cs b/src/SMAPI.Toolkit/ModToolkit.cs index 4b026b7a..08fe0fed 100644 --- a/src/SMAPI.Toolkit/ModToolkit.cs +++ b/src/SMAPI.Toolkit/ModToolkit.cs @@ -8,7 +8,7 @@ using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; using StardewModdingAPI.Toolkit.Framework.GameScanning; using StardewModdingAPI.Toolkit.Framework.ModData; using StardewModdingAPI.Toolkit.Framework.ModScanning; -using StardewModdingAPI.Toolkit.Serialisation; +using StardewModdingAPI.Toolkit.Serialization; namespace StardewModdingAPI.Toolkit { diff --git a/src/SMAPI.Toolkit/SemanticVersion.cs b/src/SMAPI.Toolkit/SemanticVersion.cs index 78b4c007..6d5d65ac 100644 --- a/src/SMAPI.Toolkit/SemanticVersion.cs +++ b/src/SMAPI.Toolkit/SemanticVersion.cs @@ -56,7 +56,7 @@ namespace StardewModdingAPI.Toolkit this.MajorVersion = major; this.MinorVersion = minor; this.PatchVersion = patch; - this.PrereleaseTag = this.GetNormalisedTag(prereleaseTag); + this.PrereleaseTag = this.GetNormalizedTag(prereleaseTag); this.AssertValid(); } @@ -89,16 +89,16 @@ namespace StardewModdingAPI.Toolkit if (!match.Success) throw new FormatException($"The input '{version}' isn't a valid semantic version."); - // initialise + // initialize this.MajorVersion = int.Parse(match.Groups["major"].Value); this.MinorVersion = match.Groups["minor"].Success ? int.Parse(match.Groups["minor"].Value) : 0; this.PatchVersion = match.Groups["patch"].Success ? int.Parse(match.Groups["patch"].Value) : 0; - this.PrereleaseTag = match.Groups["prerelease"].Success ? this.GetNormalisedTag(match.Groups["prerelease"].Value) : null; + this.PrereleaseTag = match.Groups["prerelease"].Success ? this.GetNormalizedTag(match.Groups["prerelease"].Value) : null; this.AssertValid(); } - /// Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version. + /// Get an integer indicating whether this version precedes (less than 0), supersedes (more than 0), or is equivalent to (0) the specified version. /// The version to compare with this instance. /// The value is null. public int CompareTo(ISemanticVersion other) @@ -116,7 +116,7 @@ namespace StardewModdingAPI.Toolkit return other != null && this.CompareTo(other) == 0; } - /// Whether this is a pre-release version. + /// Whether this is a prerelease version. public bool IsPrerelease() { return !string.IsNullOrWhiteSpace(this.PrereleaseTag); @@ -206,15 +206,15 @@ namespace StardewModdingAPI.Toolkit /********* ** Private methods *********/ - /// Get a normalised build tag. - /// The tag to normalise. - private string GetNormalisedTag(string tag) + /// Get a normalized build tag. + /// The tag to normalize. + private string GetNormalizedTag(string tag) { tag = tag?.Trim(); return !string.IsNullOrWhiteSpace(tag) ? tag : null; } - /// Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version. + /// Get an integer indicating whether this version precedes (less than 0), supersedes (more than 0), or is equivalent to (0) the specified version. /// The major version to compare with this instance. /// The minor version to compare with this instance. /// The patch version to compare with this instance. @@ -235,7 +235,7 @@ namespace StardewModdingAPI.Toolkit if (this.PrereleaseTag == otherTag) return same; - // stable supercedes pre-release + // stable supersedes prerelease bool curIsStable = string.IsNullOrWhiteSpace(this.PrereleaseTag); bool otherIsStable = string.IsNullOrWhiteSpace(otherTag); if (curIsStable) @@ -243,12 +243,12 @@ namespace StardewModdingAPI.Toolkit if (otherIsStable) return curOlder; - // compare two pre-release tag values + // compare two prerelease tag values string[] curParts = this.PrereleaseTag.Split('.', '-'); string[] otherParts = otherTag.Split('.', '-'); for (int i = 0; i < curParts.Length; i++) { - // longer prerelease tag supercedes if otherwise equal + // longer prerelease tag supersedes if otherwise equal if (otherParts.Length <= i) return curNewer; diff --git a/src/SMAPI.Toolkit/Serialisation/Converters/ManifestContentPackForConverter.cs b/src/SMAPI.Toolkit/Serialisation/Converters/ManifestContentPackForConverter.cs deleted file mode 100644 index 232c22a7..00000000 --- a/src/SMAPI.Toolkit/Serialisation/Converters/ManifestContentPackForConverter.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using Newtonsoft.Json; -using StardewModdingAPI.Toolkit.Serialisation.Models; - -namespace StardewModdingAPI.Toolkit.Serialisation.Converters -{ - /// Handles deserialisation of arrays. - public class ManifestContentPackForConverter : JsonConverter - { - /********* - ** Accessors - *********/ - /// Whether this converter can write JSON. - public override bool CanWrite => false; - - - /********* - ** Public methods - *********/ - /// Get whether this instance can convert the specified object type. - /// The object type. - public override bool CanConvert(Type objectType) - { - return objectType == typeof(ManifestContentPackFor[]); - } - - - /********* - ** Protected methods - *********/ - /// Read the JSON representation of the object. - /// The JSON reader. - /// The object type. - /// The object being read. - /// The calling serializer. - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - return serializer.Deserialize(reader); - } - - /// Writes the JSON representation of the object. - /// The JSON writer. - /// The value. - /// The calling serializer. - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - throw new InvalidOperationException("This converter does not write JSON."); - } - } -} diff --git a/src/SMAPI.Toolkit/Serialisation/Converters/ManifestDependencyArrayConverter.cs b/src/SMAPI.Toolkit/Serialisation/Converters/ManifestDependencyArrayConverter.cs deleted file mode 100644 index 0a304ee3..00000000 --- a/src/SMAPI.Toolkit/Serialisation/Converters/ManifestDependencyArrayConverter.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Collections.Generic; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using StardewModdingAPI.Toolkit.Serialisation.Models; - -namespace StardewModdingAPI.Toolkit.Serialisation.Converters -{ - /// Handles deserialisation of arrays. - internal class ManifestDependencyArrayConverter : JsonConverter - { - /********* - ** Accessors - *********/ - /// Whether this converter can write JSON. - public override bool CanWrite => false; - - - /********* - ** Public methods - *********/ - /// Get whether this instance can convert the specified object type. - /// The object type. - public override bool CanConvert(Type objectType) - { - return objectType == typeof(ManifestDependency[]); - } - - - /********* - ** Protected methods - *********/ - /// Read the JSON representation of the object. - /// The JSON reader. - /// The object type. - /// The object being read. - /// The calling serializer. - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - List result = new List(); - foreach (JObject obj in JArray.Load(reader).Children()) - { - string uniqueID = obj.ValueIgnoreCase(nameof(ManifestDependency.UniqueID)); - string minVersion = obj.ValueIgnoreCase(nameof(ManifestDependency.MinimumVersion)); - bool required = obj.ValueIgnoreCase(nameof(ManifestDependency.IsRequired)) ?? true; - result.Add(new ManifestDependency(uniqueID, minVersion, required)); - } - return result.ToArray(); - } - - /// Writes the JSON representation of the object. - /// The JSON writer. - /// The value. - /// The calling serializer. - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - throw new InvalidOperationException("This converter does not write JSON."); - } - } -} diff --git a/src/SMAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs b/src/SMAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs deleted file mode 100644 index c50de4db..00000000 --- a/src/SMAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace StardewModdingAPI.Toolkit.Serialisation.Converters -{ - /// Handles deserialisation of . - internal class SemanticVersionConverter : JsonConverter - { - /********* - ** Accessors - *********/ - /// Get whether this converter can read JSON. - public override bool CanRead => true; - - /// Get whether this converter can write JSON. - public override bool CanWrite => true; - - - /********* - ** Public methods - *********/ - /// Get whether this instance can convert the specified object type. - /// The object type. - public override bool CanConvert(Type objectType) - { - return typeof(ISemanticVersion).IsAssignableFrom(objectType); - } - - /// Reads the JSON representation of the object. - /// The JSON reader. - /// The object type. - /// The object being read. - /// The calling serializer. - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - string path = reader.Path; - switch (reader.TokenType) - { - case JsonToken.StartObject: - return this.ReadObject(JObject.Load(reader)); - case JsonToken.String: - return this.ReadString(JToken.Load(reader).Value(), path); - default: - throw new SParseException($"Can't parse {nameof(ISemanticVersion)} from {reader.TokenType} node (path: {reader.Path})."); - } - } - - /// Writes the JSON representation of the object. - /// The JSON writer. - /// The value. - /// The calling serializer. - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - writer.WriteValue(value?.ToString()); - } - - - /********* - ** Private methods - *********/ - /// Read a JSON object. - /// The JSON object to read. - private ISemanticVersion ReadObject(JObject obj) - { - int major = obj.ValueIgnoreCase(nameof(ISemanticVersion.MajorVersion)); - int minor = obj.ValueIgnoreCase(nameof(ISemanticVersion.MinorVersion)); - int patch = obj.ValueIgnoreCase(nameof(ISemanticVersion.PatchVersion)); - string prereleaseTag = obj.ValueIgnoreCase(nameof(ISemanticVersion.PrereleaseTag)); - - return new SemanticVersion(major, minor, patch, prereleaseTag); - } - - /// Read a JSON string. - /// The JSON string value. - /// The path to the current JSON node. - private ISemanticVersion ReadString(string str, string path) - { - if (string.IsNullOrWhiteSpace(str)) - return null; - if (!SemanticVersion.TryParse(str, out ISemanticVersion version)) - throw new SParseException($"Can't parse semantic version from invalid value '{str}', should be formatted like 1.2, 1.2.30, or 1.2.30-beta (path: {path})."); - return version; - } - } -} diff --git a/src/SMAPI.Toolkit/Serialisation/Converters/SimpleReadOnlyConverter.cs b/src/SMAPI.Toolkit/Serialisation/Converters/SimpleReadOnlyConverter.cs deleted file mode 100644 index 5e0b0f4a..00000000 --- a/src/SMAPI.Toolkit/Serialisation/Converters/SimpleReadOnlyConverter.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace StardewModdingAPI.Toolkit.Serialisation.Converters -{ - /// The base implementation for simplified converters which deserialise without overriding serialisation. - /// The type to deserialise. - internal abstract class SimpleReadOnlyConverter : JsonConverter - { - /********* - ** Accessors - *********/ - /// Whether this converter can write JSON. - public override bool CanWrite => false; - - - /********* - ** Public methods - *********/ - /// Get whether this instance can convert the specified object type. - /// The object type. - public override bool CanConvert(Type objectType) - { - return objectType == typeof(T); - } - - /// Writes the JSON representation of the object. - /// The JSON writer. - /// The value. - /// The calling serializer. - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - throw new InvalidOperationException("This converter does not write JSON."); - } - - /// Reads the JSON representation of the object. - /// The JSON reader. - /// The object type. - /// The object being read. - /// The calling serializer. - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - string path = reader.Path; - switch (reader.TokenType) - { - case JsonToken.StartObject: - return this.ReadObject(JObject.Load(reader), path); - case JsonToken.String: - return this.ReadString(JToken.Load(reader).Value(), path); - default: - throw new SParseException($"Can't parse {typeof(T).Name} from {reader.TokenType} node (path: {reader.Path})."); - } - } - - - /********* - ** Protected methods - *********/ - /// Read a JSON object. - /// The JSON object to read. - /// The path to the current JSON node. - protected virtual T ReadObject(JObject obj, string path) - { - throw new SParseException($"Can't parse {typeof(T).Name} from object node (path: {path})."); - } - - /// Read a JSON string. - /// The JSON string value. - /// The path to the current JSON node. - protected virtual T ReadString(string str, string path) - { - throw new SParseException($"Can't parse {typeof(T).Name} from string node (path: {path})."); - } - } -} diff --git a/src/SMAPI.Toolkit/Serialisation/InternalExtensions.cs b/src/SMAPI.Toolkit/Serialisation/InternalExtensions.cs deleted file mode 100644 index 12b2c933..00000000 --- a/src/SMAPI.Toolkit/Serialisation/InternalExtensions.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using Newtonsoft.Json.Linq; - -namespace StardewModdingAPI.Toolkit.Serialisation -{ - /// Provides extension methods for parsing JSON. - public static class JsonExtensions - { - /// Get a JSON field value from a case-insensitive field name. This will check for an exact match first, then search without case sensitivity. - /// The value type. - /// The JSON object to search. - /// The field name. - public static T ValueIgnoreCase(this JObject obj, string fieldName) - { - JToken token = obj.GetValue(fieldName, StringComparison.InvariantCultureIgnoreCase); - return token != null - ? token.Value() - : default(T); - } - } -} diff --git a/src/SMAPI.Toolkit/Serialisation/JsonHelper.cs b/src/SMAPI.Toolkit/Serialisation/JsonHelper.cs deleted file mode 100644 index cf2ce0d1..00000000 --- a/src/SMAPI.Toolkit/Serialisation/JsonHelper.cs +++ /dev/null @@ -1,136 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using StardewModdingAPI.Toolkit.Serialisation.Converters; - -namespace StardewModdingAPI.Toolkit.Serialisation -{ - /// Encapsulates SMAPI's JSON file parsing. - public class JsonHelper - { - /********* - ** Accessors - *********/ - /// The JSON settings to use when serialising and deserialising files. - public JsonSerializerSettings JsonSettings { get; } = new JsonSerializerSettings - { - Formatting = Formatting.Indented, - ObjectCreationHandling = ObjectCreationHandling.Replace, // avoid issue where default ICollection values are duplicated each time the config is loaded - Converters = new List - { - new SemanticVersionConverter(), - new StringEnumConverter() - } - }; - - - /********* - ** Public methods - *********/ - /// Read a JSON file. - /// The model type. - /// The absolete file path. - /// The parsed content model. - /// Returns false if the file doesn't exist, else true. - /// The given is empty or invalid. - /// The file contains invalid JSON. - public bool ReadJsonFileIfExists(string fullPath, out TModel result) - { - // validate - if (string.IsNullOrWhiteSpace(fullPath)) - throw new ArgumentException("The file path is empty or invalid.", nameof(fullPath)); - - // read file - string json; - try - { - json = File.ReadAllText(fullPath); - } - catch (Exception ex) when (ex is DirectoryNotFoundException || ex is FileNotFoundException) - { - result = default(TModel); - return false; - } - - // deserialise model - try - { - result = this.Deserialise(json); - return true; - } - catch (Exception ex) - { - string error = $"Can't parse JSON file at {fullPath}."; - - if (ex is JsonReaderException) - { - error += " This doesn't seem to be valid JSON."; - if (json.Contains("“") || json.Contains("”")) - error += " Found curly quotes in the text; note that only straight quotes are allowed in JSON."; - } - error += $"\nTechnical details: {ex.Message}"; - throw new JsonReaderException(error); - } - } - - /// Save to a JSON file. - /// The model type. - /// The absolete file path. - /// The model to save. - /// The given path is empty or invalid. - public void WriteJsonFile(string fullPath, TModel model) - where TModel : class - { - // validate - if (string.IsNullOrWhiteSpace(fullPath)) - throw new ArgumentException("The file path is empty or invalid.", nameof(fullPath)); - - // create directory if needed - string dir = Path.GetDirectoryName(fullPath); - if (dir == null) - throw new ArgumentException("The file path is invalid.", nameof(fullPath)); - if (!Directory.Exists(dir)) - Directory.CreateDirectory(dir); - - // write file - string json = this.Serialise(model); - File.WriteAllText(fullPath, json); - } - - /// Deserialize JSON text if possible. - /// The model type. - /// The raw JSON text. - public TModel Deserialise(string json) - { - try - { - return JsonConvert.DeserializeObject(json, this.JsonSettings); - } - catch (JsonReaderException) - { - // try replacing curly quotes - if (json.Contains("“") || json.Contains("”")) - { - try - { - return JsonConvert.DeserializeObject(json.Replace('“', '"').Replace('”', '"'), this.JsonSettings); - } - catch { /* rethrow original error */ } - } - - throw; - } - } - - /// Serialize a model to JSON text. - /// The model type. - /// The model to serialise. - /// The formatting to apply. - public string Serialise(TModel model, Formatting formatting = Formatting.Indented) - { - return JsonConvert.SerializeObject(model, formatting, this.JsonSettings); - } - } -} diff --git a/src/SMAPI.Toolkit/Serialisation/Models/Manifest.cs b/src/SMAPI.Toolkit/Serialisation/Models/Manifest.cs deleted file mode 100644 index 6cb9496b..00000000 --- a/src/SMAPI.Toolkit/Serialisation/Models/Manifest.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; -using StardewModdingAPI.Toolkit.Serialisation.Converters; - -namespace StardewModdingAPI.Toolkit.Serialisation.Models -{ - /// A manifest which describes a mod for SMAPI. - public class Manifest : IManifest - { - /********* - ** Accessors - *********/ - /// The mod name. - public string Name { get; set; } - - /// A brief description of the mod. - public string Description { get; set; } - - /// The mod author's name. - public string Author { get; set; } - - /// The mod version. - public ISemanticVersion Version { get; set; } - - /// The minimum SMAPI version required by this mod, if any. - public ISemanticVersion MinimumApiVersion { get; set; } - - /// The name of the DLL in the directory that has the Entry method. Mutually exclusive with . - public string EntryDll { get; set; } - - /// The mod which will read this as a content pack. Mutually exclusive with . - [JsonConverter(typeof(ManifestContentPackForConverter))] - public IManifestContentPackFor ContentPackFor { get; set; } - - /// The other mods that must be loaded before this mod. - [JsonConverter(typeof(ManifestDependencyArrayConverter))] - public IManifestDependency[] Dependencies { get; set; } - - /// The namespaced mod IDs to query for updates (like Nexus:541). - public string[] UpdateKeys { get; set; } - - /// The unique mod ID. - public string UniqueID { get; set; } - - /// Any manifest fields which didn't match a valid field. - [JsonExtensionData] - public IDictionary ExtraFields { get; set; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - public Manifest() { } - - /// Construct an instance for a transitional content pack. - /// The unique mod ID. - /// The mod name. - /// The mod author's name. - /// A brief description of the mod. - /// The mod version. - /// The modID which will read this as a content pack. - public Manifest(string uniqueID, string name, string author, string description, ISemanticVersion version, string contentPackFor = null) - { - this.Name = name; - this.Author = author; - this.Description = description; - this.Version = version; - this.UniqueID = uniqueID; - this.UpdateKeys = new string[0]; - this.ContentPackFor = new ManifestContentPackFor { UniqueID = contentPackFor }; - } - } -} diff --git a/src/SMAPI.Toolkit/Serialisation/Models/ManifestContentPackFor.cs b/src/SMAPI.Toolkit/Serialisation/Models/ManifestContentPackFor.cs deleted file mode 100644 index d0e42216..00000000 --- a/src/SMAPI.Toolkit/Serialisation/Models/ManifestContentPackFor.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace StardewModdingAPI.Toolkit.Serialisation.Models -{ - /// Indicates which mod can read the content pack represented by the containing manifest. - public class ManifestContentPackFor : IManifestContentPackFor - { - /********* - ** Accessors - *********/ - /// The unique ID of the mod which can read this content pack. - public string UniqueID { get; set; } - - /// The minimum required version (if any). - public ISemanticVersion MinimumVersion { get; set; } - } -} diff --git a/src/SMAPI.Toolkit/Serialisation/Models/ManifestDependency.cs b/src/SMAPI.Toolkit/Serialisation/Models/ManifestDependency.cs deleted file mode 100644 index 8db58d5d..00000000 --- a/src/SMAPI.Toolkit/Serialisation/Models/ManifestDependency.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace StardewModdingAPI.Toolkit.Serialisation.Models -{ - /// A mod dependency listed in a mod manifest. - public class ManifestDependency : IManifestDependency - { - /********* - ** Accessors - *********/ - /// The unique mod ID to require. - public string UniqueID { get; set; } - - /// The minimum required version (if any). - public ISemanticVersion MinimumVersion { get; set; } - - /// Whether the dependency must be installed to use the mod. - public bool IsRequired { get; set; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The unique mod ID to require. - /// The minimum required version (if any). - /// Whether the dependency must be installed to use the mod. - public ManifestDependency(string uniqueID, string minimumVersion, bool required = true) - { - this.UniqueID = uniqueID; - this.MinimumVersion = !string.IsNullOrWhiteSpace(minimumVersion) - ? new SemanticVersion(minimumVersion) - : null; - this.IsRequired = required; - } - } -} diff --git a/src/SMAPI.Toolkit/Serialisation/SParseException.cs b/src/SMAPI.Toolkit/Serialisation/SParseException.cs deleted file mode 100644 index 61a7b305..00000000 --- a/src/SMAPI.Toolkit/Serialisation/SParseException.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace StardewModdingAPI.Toolkit.Serialisation -{ - /// A format exception which provides a user-facing error message. - internal class SParseException : FormatException - { - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The error message. - /// The underlying exception, if any. - public SParseException(string message, Exception ex = null) - : base(message, ex) { } - } -} diff --git a/src/SMAPI.Toolkit/Serialization/Converters/ManifestContentPackForConverter.cs b/src/SMAPI.Toolkit/Serialization/Converters/ManifestContentPackForConverter.cs new file mode 100644 index 00000000..5cabe9d8 --- /dev/null +++ b/src/SMAPI.Toolkit/Serialization/Converters/ManifestContentPackForConverter.cs @@ -0,0 +1,50 @@ +using System; +using Newtonsoft.Json; +using StardewModdingAPI.Toolkit.Serialization.Models; + +namespace StardewModdingAPI.Toolkit.Serialization.Converters +{ + /// Handles deserialization of arrays. + public class ManifestContentPackForConverter : JsonConverter + { + /********* + ** Accessors + *********/ + /// Whether this converter can write JSON. + public override bool CanWrite => false; + + + /********* + ** Public methods + *********/ + /// Get whether this instance can convert the specified object type. + /// The object type. + public override bool CanConvert(Type objectType) + { + return objectType == typeof(ManifestContentPackFor[]); + } + + + /********* + ** Protected methods + *********/ + /// Read the JSON representation of the object. + /// The JSON reader. + /// The object type. + /// The object being read. + /// The calling serializer. + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + return serializer.Deserialize(reader); + } + + /// Writes the JSON representation of the object. + /// The JSON writer. + /// The value. + /// The calling serializer. + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new InvalidOperationException("This converter does not write JSON."); + } + } +} diff --git a/src/SMAPI.Toolkit/Serialization/Converters/ManifestDependencyArrayConverter.cs b/src/SMAPI.Toolkit/Serialization/Converters/ManifestDependencyArrayConverter.cs new file mode 100644 index 00000000..7b88d6b7 --- /dev/null +++ b/src/SMAPI.Toolkit/Serialization/Converters/ManifestDependencyArrayConverter.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using StardewModdingAPI.Toolkit.Serialization.Models; + +namespace StardewModdingAPI.Toolkit.Serialization.Converters +{ + /// Handles deserialization of arrays. + internal class ManifestDependencyArrayConverter : JsonConverter + { + /********* + ** Accessors + *********/ + /// Whether this converter can write JSON. + public override bool CanWrite => false; + + + /********* + ** Public methods + *********/ + /// Get whether this instance can convert the specified object type. + /// The object type. + public override bool CanConvert(Type objectType) + { + return objectType == typeof(ManifestDependency[]); + } + + + /********* + ** Protected methods + *********/ + /// Read the JSON representation of the object. + /// The JSON reader. + /// The object type. + /// The object being read. + /// The calling serializer. + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + List result = new List(); + foreach (JObject obj in JArray.Load(reader).Children()) + { + string uniqueID = obj.ValueIgnoreCase(nameof(ManifestDependency.UniqueID)); + string minVersion = obj.ValueIgnoreCase(nameof(ManifestDependency.MinimumVersion)); + bool required = obj.ValueIgnoreCase(nameof(ManifestDependency.IsRequired)) ?? true; + result.Add(new ManifestDependency(uniqueID, minVersion, required)); + } + return result.ToArray(); + } + + /// Writes the JSON representation of the object. + /// The JSON writer. + /// The value. + /// The calling serializer. + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new InvalidOperationException("This converter does not write JSON."); + } + } +} diff --git a/src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs b/src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs new file mode 100644 index 00000000..ece4a72e --- /dev/null +++ b/src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs @@ -0,0 +1,86 @@ +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace StardewModdingAPI.Toolkit.Serialization.Converters +{ + /// Handles deserialization of . + internal class SemanticVersionConverter : JsonConverter + { + /********* + ** Accessors + *********/ + /// Get whether this converter can read JSON. + public override bool CanRead => true; + + /// Get whether this converter can write JSON. + public override bool CanWrite => true; + + + /********* + ** Public methods + *********/ + /// Get whether this instance can convert the specified object type. + /// The object type. + public override bool CanConvert(Type objectType) + { + return typeof(ISemanticVersion).IsAssignableFrom(objectType); + } + + /// Reads the JSON representation of the object. + /// The JSON reader. + /// The object type. + /// The object being read. + /// The calling serializer. + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + string path = reader.Path; + switch (reader.TokenType) + { + case JsonToken.StartObject: + return this.ReadObject(JObject.Load(reader)); + case JsonToken.String: + return this.ReadString(JToken.Load(reader).Value(), path); + default: + throw new SParseException($"Can't parse {nameof(ISemanticVersion)} from {reader.TokenType} node (path: {reader.Path})."); + } + } + + /// Writes the JSON representation of the object. + /// The JSON writer. + /// The value. + /// The calling serializer. + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + writer.WriteValue(value?.ToString()); + } + + + /********* + ** Private methods + *********/ + /// Read a JSON object. + /// The JSON object to read. + private ISemanticVersion ReadObject(JObject obj) + { + int major = obj.ValueIgnoreCase(nameof(ISemanticVersion.MajorVersion)); + int minor = obj.ValueIgnoreCase(nameof(ISemanticVersion.MinorVersion)); + int patch = obj.ValueIgnoreCase(nameof(ISemanticVersion.PatchVersion)); + string prereleaseTag = obj.ValueIgnoreCase(nameof(ISemanticVersion.PrereleaseTag)); + + return new SemanticVersion(major, minor, patch, prereleaseTag); + } + + /// Read a JSON string. + /// The JSON string value. + /// The path to the current JSON node. + private ISemanticVersion ReadString(string str, string path) + { + if (string.IsNullOrWhiteSpace(str)) + return null; + if (!SemanticVersion.TryParse(str, out ISemanticVersion version)) + throw new SParseException($"Can't parse semantic version from invalid value '{str}', should be formatted like 1.2, 1.2.30, or 1.2.30-beta (path: {path})."); + return version; + } + } +} diff --git a/src/SMAPI.Toolkit/Serialization/Converters/SimpleReadOnlyConverter.cs b/src/SMAPI.Toolkit/Serialization/Converters/SimpleReadOnlyConverter.cs new file mode 100644 index 00000000..549f0c18 --- /dev/null +++ b/src/SMAPI.Toolkit/Serialization/Converters/SimpleReadOnlyConverter.cs @@ -0,0 +1,76 @@ +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace StardewModdingAPI.Toolkit.Serialization.Converters +{ + /// The base implementation for simplified converters which deserialize without overriding serialization. + /// The type to deserialize. + internal abstract class SimpleReadOnlyConverter : JsonConverter + { + /********* + ** Accessors + *********/ + /// Whether this converter can write JSON. + public override bool CanWrite => false; + + + /********* + ** Public methods + *********/ + /// Get whether this instance can convert the specified object type. + /// The object type. + public override bool CanConvert(Type objectType) + { + return objectType == typeof(T); + } + + /// Writes the JSON representation of the object. + /// The JSON writer. + /// The value. + /// The calling serializer. + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new InvalidOperationException("This converter does not write JSON."); + } + + /// Reads the JSON representation of the object. + /// The JSON reader. + /// The object type. + /// The object being read. + /// The calling serializer. + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + string path = reader.Path; + switch (reader.TokenType) + { + case JsonToken.StartObject: + return this.ReadObject(JObject.Load(reader), path); + case JsonToken.String: + return this.ReadString(JToken.Load(reader).Value(), path); + default: + throw new SParseException($"Can't parse {typeof(T).Name} from {reader.TokenType} node (path: {reader.Path})."); + } + } + + + /********* + ** Protected methods + *********/ + /// Read a JSON object. + /// The JSON object to read. + /// The path to the current JSON node. + protected virtual T ReadObject(JObject obj, string path) + { + throw new SParseException($"Can't parse {typeof(T).Name} from object node (path: {path})."); + } + + /// Read a JSON string. + /// The JSON string value. + /// The path to the current JSON node. + protected virtual T ReadString(string str, string path) + { + throw new SParseException($"Can't parse {typeof(T).Name} from string node (path: {path})."); + } + } +} diff --git a/src/SMAPI.Toolkit/Serialization/InternalExtensions.cs b/src/SMAPI.Toolkit/Serialization/InternalExtensions.cs new file mode 100644 index 00000000..9aba53bf --- /dev/null +++ b/src/SMAPI.Toolkit/Serialization/InternalExtensions.cs @@ -0,0 +1,21 @@ +using System; +using Newtonsoft.Json.Linq; + +namespace StardewModdingAPI.Toolkit.Serialization +{ + /// Provides extension methods for parsing JSON. + public static class JsonExtensions + { + /// Get a JSON field value from a case-insensitive field name. This will check for an exact match first, then search without case sensitivity. + /// The value type. + /// The JSON object to search. + /// The field name. + public static T ValueIgnoreCase(this JObject obj, string fieldName) + { + JToken token = obj.GetValue(fieldName, StringComparison.InvariantCultureIgnoreCase); + return token != null + ? token.Value() + : default(T); + } + } +} diff --git a/src/SMAPI.Toolkit/Serialization/JsonHelper.cs b/src/SMAPI.Toolkit/Serialization/JsonHelper.cs new file mode 100644 index 00000000..031afbb0 --- /dev/null +++ b/src/SMAPI.Toolkit/Serialization/JsonHelper.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using StardewModdingAPI.Toolkit.Serialization.Converters; + +namespace StardewModdingAPI.Toolkit.Serialization +{ + /// Encapsulates SMAPI's JSON file parsing. + public class JsonHelper + { + /********* + ** Accessors + *********/ + /// The JSON settings to use when serializing and deserializing files. + public JsonSerializerSettings JsonSettings { get; } = new JsonSerializerSettings + { + Formatting = Formatting.Indented, + ObjectCreationHandling = ObjectCreationHandling.Replace, // avoid issue where default ICollection values are duplicated each time the config is loaded + Converters = new List + { + new SemanticVersionConverter(), + new StringEnumConverter() + } + }; + + + /********* + ** Public methods + *********/ + /// Read a JSON file. + /// The model type. + /// The absolute file path. + /// The parsed content model. + /// Returns false if the file doesn't exist, else true. + /// The given is empty or invalid. + /// The file contains invalid JSON. + public bool ReadJsonFileIfExists(string fullPath, out TModel result) + { + // validate + if (string.IsNullOrWhiteSpace(fullPath)) + throw new ArgumentException("The file path is empty or invalid.", nameof(fullPath)); + + // read file + string json; + try + { + json = File.ReadAllText(fullPath); + } + catch (Exception ex) when (ex is DirectoryNotFoundException || ex is FileNotFoundException) + { + result = default(TModel); + return false; + } + + // deserialize model + try + { + result = this.Deserialize(json); + return true; + } + catch (Exception ex) + { + string error = $"Can't parse JSON file at {fullPath}."; + + if (ex is JsonReaderException) + { + error += " This doesn't seem to be valid JSON."; + if (json.Contains("“") || json.Contains("”")) + error += " Found curly quotes in the text; note that only straight quotes are allowed in JSON."; + } + error += $"\nTechnical details: {ex.Message}"; + throw new JsonReaderException(error); + } + } + + /// Save to a JSON file. + /// The model type. + /// The absolute file path. + /// The model to save. + /// The given path is empty or invalid. + public void WriteJsonFile(string fullPath, TModel model) + where TModel : class + { + // validate + if (string.IsNullOrWhiteSpace(fullPath)) + throw new ArgumentException("The file path is empty or invalid.", nameof(fullPath)); + + // create directory if needed + string dir = Path.GetDirectoryName(fullPath); + if (dir == null) + throw new ArgumentException("The file path is invalid.", nameof(fullPath)); + if (!Directory.Exists(dir)) + Directory.CreateDirectory(dir); + + // write file + string json = this.Serialize(model); + File.WriteAllText(fullPath, json); + } + + /// Deserialize JSON text if possible. + /// The model type. + /// The raw JSON text. + public TModel Deserialize(string json) + { + try + { + return JsonConvert.DeserializeObject(json, this.JsonSettings); + } + catch (JsonReaderException) + { + // try replacing curly quotes + if (json.Contains("“") || json.Contains("”")) + { + try + { + return JsonConvert.DeserializeObject(json.Replace('“', '"').Replace('”', '"'), this.JsonSettings); + } + catch { /* rethrow original error */ } + } + + throw; + } + } + + /// Serialize a model to JSON text. + /// The model type. + /// The model to serialize. + /// The formatting to apply. + public string Serialize(TModel model, Formatting formatting = Formatting.Indented) + { + return JsonConvert.SerializeObject(model, formatting, this.JsonSettings); + } + } +} diff --git a/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs b/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs new file mode 100644 index 00000000..99e85cbd --- /dev/null +++ b/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs @@ -0,0 +1,74 @@ +using System.Collections.Generic; +using Newtonsoft.Json; +using StardewModdingAPI.Toolkit.Serialization.Converters; + +namespace StardewModdingAPI.Toolkit.Serialization.Models +{ + /// A manifest which describes a mod for SMAPI. + public class Manifest : IManifest + { + /********* + ** Accessors + *********/ + /// The mod name. + public string Name { get; set; } + + /// A brief description of the mod. + public string Description { get; set; } + + /// The mod author's name. + public string Author { get; set; } + + /// The mod version. + public ISemanticVersion Version { get; set; } + + /// The minimum SMAPI version required by this mod, if any. + public ISemanticVersion MinimumApiVersion { get; set; } + + /// The name of the DLL in the directory that has the Entry method. Mutually exclusive with . + public string EntryDll { get; set; } + + /// The mod which will read this as a content pack. Mutually exclusive with . + [JsonConverter(typeof(ManifestContentPackForConverter))] + public IManifestContentPackFor ContentPackFor { get; set; } + + /// The other mods that must be loaded before this mod. + [JsonConverter(typeof(ManifestDependencyArrayConverter))] + public IManifestDependency[] Dependencies { get; set; } + + /// The namespaced mod IDs to query for updates (like Nexus:541). + public string[] UpdateKeys { get; set; } + + /// The unique mod ID. + public string UniqueID { get; set; } + + /// Any manifest fields which didn't match a valid field. + [JsonExtensionData] + public IDictionary ExtraFields { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public Manifest() { } + + /// Construct an instance for a transitional content pack. + /// The unique mod ID. + /// The mod name. + /// The mod author's name. + /// A brief description of the mod. + /// The mod version. + /// The modID which will read this as a content pack. + public Manifest(string uniqueID, string name, string author, string description, ISemanticVersion version, string contentPackFor = null) + { + this.Name = name; + this.Author = author; + this.Description = description; + this.Version = version; + this.UniqueID = uniqueID; + this.UpdateKeys = new string[0]; + this.ContentPackFor = new ManifestContentPackFor { UniqueID = contentPackFor }; + } + } +} diff --git a/src/SMAPI.Toolkit/Serialization/Models/ManifestContentPackFor.cs b/src/SMAPI.Toolkit/Serialization/Models/ManifestContentPackFor.cs new file mode 100644 index 00000000..1eb80889 --- /dev/null +++ b/src/SMAPI.Toolkit/Serialization/Models/ManifestContentPackFor.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Toolkit.Serialization.Models +{ + /// Indicates which mod can read the content pack represented by the containing manifest. + public class ManifestContentPackFor : IManifestContentPackFor + { + /********* + ** Accessors + *********/ + /// The unique ID of the mod which can read this content pack. + public string UniqueID { get; set; } + + /// The minimum required version (if any). + public ISemanticVersion MinimumVersion { get; set; } + } +} diff --git a/src/SMAPI.Toolkit/Serialization/Models/ManifestDependency.cs b/src/SMAPI.Toolkit/Serialization/Models/ManifestDependency.cs new file mode 100644 index 00000000..00f168f4 --- /dev/null +++ b/src/SMAPI.Toolkit/Serialization/Models/ManifestDependency.cs @@ -0,0 +1,35 @@ +namespace StardewModdingAPI.Toolkit.Serialization.Models +{ + /// A mod dependency listed in a mod manifest. + public class ManifestDependency : IManifestDependency + { + /********* + ** Accessors + *********/ + /// The unique mod ID to require. + public string UniqueID { get; set; } + + /// The minimum required version (if any). + public ISemanticVersion MinimumVersion { get; set; } + + /// Whether the dependency must be installed to use the mod. + public bool IsRequired { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The unique mod ID to require. + /// The minimum required version (if any). + /// Whether the dependency must be installed to use the mod. + public ManifestDependency(string uniqueID, string minimumVersion, bool required = true) + { + this.UniqueID = uniqueID; + this.MinimumVersion = !string.IsNullOrWhiteSpace(minimumVersion) + ? new SemanticVersion(minimumVersion) + : null; + this.IsRequired = required; + } + } +} diff --git a/src/SMAPI.Toolkit/Serialization/SParseException.cs b/src/SMAPI.Toolkit/Serialization/SParseException.cs new file mode 100644 index 00000000..5f58b5b8 --- /dev/null +++ b/src/SMAPI.Toolkit/Serialization/SParseException.cs @@ -0,0 +1,17 @@ +using System; + +namespace StardewModdingAPI.Toolkit.Serialization +{ + /// A format exception which provides a user-facing error message. + internal class SParseException : FormatException + { + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The error message. + /// The underlying exception, if any. + public SParseException(string message, Exception ex = null) + : base(message, ex) { } + } +} diff --git a/src/SMAPI.Toolkit/Utilities/PathUtilities.cs b/src/SMAPI.Toolkit/Utilities/PathUtilities.cs index 8a3c2b03..40a59d87 100644 --- a/src/SMAPI.Toolkit/Utilities/PathUtilities.cs +++ b/src/SMAPI.Toolkit/Utilities/PathUtilities.cs @@ -6,7 +6,7 @@ using System.Text.RegularExpressions; namespace StardewModdingAPI.Toolkit.Utilities { - /// Provides utilities for normalising file paths. + /// Provides utilities for normalizing file paths. public static class PathUtilities { /********* @@ -15,14 +15,14 @@ namespace StardewModdingAPI.Toolkit.Utilities /// The possible directory separator characters in a file path. private static readonly char[] PossiblePathSeparators = new[] { '/', '\\', Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }.Distinct().ToArray(); - /// The preferred directory separator chaeacter in an asset key. + /// The preferred directory separator character in an asset key. private static readonly string PreferredPathSeparator = Path.DirectorySeparatorChar.ToString(); /********* ** Public methods *********/ - /// Get the segments from a path (e.g. /usr/bin/boop => usr, bin, and boop). + /// Get the segments from a path (e.g. /usr/bin/example => usr, bin, and example). /// The path to split. /// The number of segments to match. Any additional segments will be merged into the last returned part. public static string[] GetSegments(string path, int? limit = null) @@ -32,16 +32,16 @@ namespace StardewModdingAPI.Toolkit.Utilities : path.Split(PathUtilities.PossiblePathSeparators, StringSplitOptions.RemoveEmptyEntries); } - /// Normalise path separators in a file path. - /// The file path to normalise. + /// Normalize path separators in a file path. + /// The file path to normalize. [Pure] - public static string NormalisePathSeparators(string path) + public static string NormalizePathSeparators(string path) { string[] parts = PathUtilities.GetSegments(path); - string normalised = string.Join(PathUtilities.PreferredPathSeparator, parts); + string normalized = string.Join(PathUtilities.PreferredPathSeparator, parts); if (path.StartsWith(PathUtilities.PreferredPathSeparator)) - normalised = PathUtilities.PreferredPathSeparator + normalised; // keep root slash - return normalised; + normalized = PathUtilities.PreferredPathSeparator + normalized; // keep root slash + return normalized; } /// Get a directory or file path relative to a given source path. @@ -57,7 +57,7 @@ namespace StardewModdingAPI.Toolkit.Utilities throw new InvalidOperationException($"Can't get path for '{targetPath}' relative to '{sourceDir}'."); // get relative path - string relative = PathUtilities.NormalisePathSeparators(Uri.UnescapeDataString(from.MakeRelativeUri(to).ToString())); + string relative = PathUtilities.NormalizePathSeparators(Uri.UnescapeDataString(from.MakeRelativeUri(to).ToString())); if (relative == "") relative = "./"; return relative; diff --git a/src/SMAPI.Web/BackgroundService.cs b/src/SMAPI.Web/BackgroundService.cs index dfd2c1b9..cb400fbe 100644 --- a/src/SMAPI.Web/BackgroundService.cs +++ b/src/SMAPI.Web/BackgroundService.cs @@ -11,7 +11,7 @@ using StardewModdingAPI.Web.Framework.Caching.Wiki; namespace StardewModdingAPI.Web { /// A hosted service which runs background data updates. - /// Task methods need to be static, since otherwise Hangfire will try to serialise the entire instance. + /// Task methods need to be static, since otherwise Hangfire will try to serialize the entire instance. internal class BackgroundService : IHostedService, IDisposable { /********* @@ -94,8 +94,8 @@ namespace StardewModdingAPI.Web /********* ** Private method *********/ - /// Initialise the background service if it's not already initialised. - /// The background service is already initialised. + /// Initialize the background service if it's not already initialized. + /// The background service is already initialized. private void TryInit() { if (BackgroundService.JobServer != null) diff --git a/src/SMAPI.Web/Controllers/JsonValidatorController.cs b/src/SMAPI.Web/Controllers/JsonValidatorController.cs index d82765e7..31471141 100644 --- a/src/SMAPI.Web/Controllers/JsonValidatorController.cs +++ b/src/SMAPI.Web/Controllers/JsonValidatorController.cs @@ -79,7 +79,7 @@ namespace StardewModdingAPI.Web.Controllers [Route("json/{schemaName}/{id}")] public async Task Index(string schemaName = null, string id = null) { - schemaName = this.NormaliseSchemaName(schemaName); + schemaName = this.NormalizeSchemaName(schemaName); var result = new JsonValidatorModel(this.SectionUrl, id, schemaName, this.SchemaFormats); if (string.IsNullOrWhiteSpace(id)) @@ -143,8 +143,8 @@ namespace StardewModdingAPI.Web.Controllers if (request == null) return this.View("Index", new JsonValidatorModel(this.SectionUrl, null, null, this.SchemaFormats).SetUploadError("The request seems to be invalid.")); - // normalise schema name - string schemaName = this.NormaliseSchemaName(request.SchemaName); + // normalize schema name + string schemaName = this.NormalizeSchemaName(request.SchemaName); // get raw log text string input = request.Content; @@ -178,9 +178,9 @@ namespace StardewModdingAPI.Web.Controllers return response; } - /// Get a normalised schema name, or the if blank. - /// The raw schema name to normalise. - private string NormaliseSchemaName(string schemaName) + /// Get a normalized schema name, or the if blank. + /// The raw schema name to normalize. + private string NormalizeSchemaName(string schemaName) { schemaName = schemaName?.Trim().ToLower(); return !string.IsNullOrWhiteSpace(schemaName) @@ -192,7 +192,7 @@ namespace StardewModdingAPI.Web.Controllers /// The schema ID. private FileInfo FindSchemaFile(string id) { - // normalise ID + // normalize ID id = id?.Trim().ToLower(); if (string.IsNullOrWhiteSpace(id)) return null; diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index a7398eee..8419b220 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -120,7 +120,7 @@ namespace StardewModdingAPI.Web.Controllers /// Returns the mod data if found, else null. private async Task GetModData(ModSearchEntryModel search, WikiModEntry[] wikiData, bool includeExtendedMetadata) { - // crossreference data + // cross-reference data ModDataRecord record = this.ModDatabase.Get(search.ID); WikiModEntry wikiEntry = wikiData.FirstOrDefault(entry => entry.ID.Contains(search.ID.Trim(), StringComparer.InvariantCultureIgnoreCase)); UpdateKey[] updateKeys = this.GetUpdateKeys(search.UpdateKeys, record, wikiEntry).ToArray(); diff --git a/src/SMAPI.Web/Framework/AllowLargePostsAttribute.cs b/src/SMAPI.Web/Framework/AllowLargePostsAttribute.cs index 5dc0feb6..864aa215 100644 --- a/src/SMAPI.Web/Framework/AllowLargePostsAttribute.cs +++ b/src/SMAPI.Web/Framework/AllowLargePostsAttribute.cs @@ -36,7 +36,7 @@ namespace StardewModdingAPI.Web.Framework } /// Called early in the filter pipeline to confirm request is authorized. - /// The authorisation filter context. + /// The authorization filter context. public void OnAuthorization(AuthorizationFilterContext context) { IFeatureCollection features = context.HttpContext.Features; diff --git a/src/SMAPI.Web/Framework/Caching/Mods/ModCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/Mods/ModCacheRepository.cs index 4258cc85..2e7804a7 100644 --- a/src/SMAPI.Web/Framework/Caching/Mods/ModCacheRepository.cs +++ b/src/SMAPI.Web/Framework/Caching/Mods/ModCacheRepository.cs @@ -40,7 +40,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Mods public bool TryGetMod(ModRepositoryKey site, string id, out CachedMod mod, bool markRequested = true) { // get mod - id = this.NormaliseId(id); + id = this.NormalizeId(id); mod = this.Mods.Find(entry => entry.ID == id && entry.Site == site).FirstOrDefault(); if (mod == null) return false; @@ -62,7 +62,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Mods /// The stored mod record. public void SaveMod(ModRepositoryKey site, string id, ModInfoModel mod, out CachedMod cachedMod) { - id = this.NormaliseId(id); + id = this.NormalizeId(id); cachedMod = this.SaveMod(new CachedMod(site, id, mod)); } @@ -83,7 +83,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Mods /// The mod data. public CachedMod SaveMod(CachedMod mod) { - string id = this.NormaliseId(mod.ID); + string id = this.NormalizeId(mod.ID); this.Mods.ReplaceOne( entry => entry.ID == id && entry.Site == mod.Site, @@ -94,9 +94,9 @@ namespace StardewModdingAPI.Web.Framework.Caching.Mods return mod; } - /// Normalise a mod ID for case-insensitive search. + /// Normalize a mod ID for case-insensitive search. /// The mod ID. - public string NormaliseId(string id) + public string NormalizeId(string id) { return id.Trim().ToLower(); } diff --git a/src/SMAPI.Web/Framework/Caching/UtcDateTimeOffsetSerializer.cs b/src/SMAPI.Web/Framework/Caching/UtcDateTimeOffsetSerializer.cs index ad95a975..6a103e37 100644 --- a/src/SMAPI.Web/Framework/Caching/UtcDateTimeOffsetSerializer.cs +++ b/src/SMAPI.Web/Framework/Caching/UtcDateTimeOffsetSerializer.cs @@ -5,7 +5,7 @@ using MongoDB.Bson.Serialization.Serializers; namespace StardewModdingAPI.Web.Framework.Caching { - /// Serialises to a UTC date field instead of the default array. + /// Serializes to a UTC date field instead of the default array. public class UtcDateTimeOffsetSerializer : StructSerializerBase { /********* diff --git a/src/SMAPI.Web/Framework/JobDashboardAuthorizationFilter.cs b/src/SMAPI.Web/Framework/JobDashboardAuthorizationFilter.cs index 9471d5fe..385c0c91 100644 --- a/src/SMAPI.Web/Framework/JobDashboardAuthorizationFilter.cs +++ b/src/SMAPI.Web/Framework/JobDashboardAuthorizationFilter.cs @@ -2,7 +2,7 @@ using Hangfire.Dashboard; namespace StardewModdingAPI.Web.Framework { - /// Authorises requests to access the Hangfire job dashboard. + /// Authorizes requests to access the Hangfire job dashboard. internal class JobDashboardAuthorizationFilter : IDashboardAuthorizationFilter { /********* @@ -15,7 +15,7 @@ namespace StardewModdingAPI.Web.Framework /********* ** Public methods *********/ - /// Authorise a request. + /// Authorize a request. /// The dashboard context. public bool Authorize(DashboardContext context) { diff --git a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs index 595e6b49..66a3687f 100644 --- a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs +++ b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs @@ -221,7 +221,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing } } - // finalise log + // finalize log gameMod.Version = log.GameVersion; log.Mods = new[] { gameMod, smapiMod }.Concat(mods.Values.OrderBy(p => p.Name)).ToArray(); return log; diff --git a/src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs index 94256005..f9f9f47d 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs @@ -34,9 +34,9 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories this.VendorKey = vendorKey; } - /// Normalise a version string. - /// The version to normalise. - protected string NormaliseVersion(string version) + /// Normalize a version string. + /// The version to normalize. + protected string NormalizeVersion(string version) { if (string.IsNullOrWhiteSpace(version)) return null; diff --git a/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs index c14fb45d..0945735a 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs @@ -39,7 +39,7 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories { var mod = await this.Client.GetModAsync(realID); return mod != null - ? new ModInfoModel(name: mod.Name, version: this.NormaliseVersion(mod.Version), url: mod.Url) + ? new ModInfoModel(name: mod.Name, version: this.NormalizeVersion(mod.Version), url: mod.Url) : new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, "Found no Chucklefish mod with this ID."); } catch (Exception ex) diff --git a/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs index e06a2497..c62cb73f 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs @@ -65,7 +65,7 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories } // return data - return result.SetVersions(version: this.NormaliseVersion(latest.Tag), previewVersion: this.NormaliseVersion(preview?.Tag)); + return result.SetVersions(version: this.NormalizeVersion(latest.Tag), previewVersion: this.NormalizeVersion(preview?.Tag)); } catch (Exception ex) { diff --git a/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs index b4791f56..9551258c 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs @@ -48,7 +48,7 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories return new ModInfoModel().SetError(remoteStatus, mod.Error); } - return new ModInfoModel(name: mod.Name, version: this.NormaliseVersion(mod.Version), previewVersion: mod.LatestFileVersion?.ToString(), url: mod.Url); + return new ModInfoModel(name: mod.Name, version: this.NormalizeVersion(mod.Version), previewVersion: mod.LatestFileVersion?.ToString(), url: mod.Url); } catch (Exception ex) { diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index de45b8a4..da5c1f1b 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -10,7 +10,7 @@ using Microsoft.Extensions.DependencyInjection; using MongoDB.Bson.Serialization; using MongoDB.Driver; using Newtonsoft.Json; -using StardewModdingAPI.Toolkit.Serialisation; +using StardewModdingAPI.Toolkit.Serialization; using StardewModdingAPI.Web.Framework; using StardewModdingAPI.Web.Framework.Caching; using StardewModdingAPI.Web.Framework.Caching.Mods; diff --git a/src/SMAPI.Web/ViewModels/LogParserModel.cs b/src/SMAPI.Web/ViewModels/LogParserModel.cs index 41864c99..25493a29 100644 --- a/src/SMAPI.Web/ViewModels/LogParserModel.cs +++ b/src/SMAPI.Web/ViewModels/LogParserModel.cs @@ -81,7 +81,7 @@ namespace StardewModdingAPI.Web.ViewModels .ToDictionary(group => group.Key, group => group.ToArray()); } - /// Get a sanitised mod name that's safe to use in anchors, attributes, and URLs. + /// Get a sanitized mod name that's safe to use in anchors, attributes, and URLs. /// The mod name. public string GetSlug(string modName) { diff --git a/src/SMAPI.Web/wwwroot/Content/js/json-validator.js b/src/SMAPI.Web/wwwroot/Content/js/json-validator.js index 265e0c5e..5499cef6 100644 --- a/src/SMAPI.Web/wwwroot/Content/js/json-validator.js +++ b/src/SMAPI.Web/wwwroot/Content/js/json-validator.js @@ -120,7 +120,7 @@ smapi.jsonValidator = function (sectionUrl, pasteID) { }; /** - * Initialise the JSON validator page. + * Initialize the JSON validator page. */ var init = function () { // set initial code formatting diff --git a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json index 315a1fb2..e12be408 100644 --- a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json +++ b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json @@ -107,7 +107,7 @@ }, "Target": { "title": "Target asset", - "description": "The game asset you want to patch (or multiple comma-delimited assets). This is the file path inside your game's Content folder, without the file extension or language (like Animals/Dinosaur to edit Content/Animals/Dinosaur.xnb). This field supports tokens and capitalisation doesn't matter. Your changes are applied in all languages unless you specify a language condition.", + "description": "The game asset you want to patch (or multiple comma-delimited assets). This is the file path inside your game's Content folder, without the file extension or language (like Animals/Dinosaur to edit Content/Animals/Dinosaur.xnb). This field supports tokens and capitalization doesn't matter. Your changes are applied in all languages unless you specify a language condition.", "type": "string", "not": { "pattern": "^ *[cC][oO][nN][tT][eE][nN][tT]/|\\.[xX][nN][bB] *$|\\.[a-zA-Z][a-zA-Z]-[a-zA-Z][a-zA-Z](?:.xnb)? *$" @@ -140,7 +140,7 @@ }, "FromFile": { "title": "Source file", - "description": "The relative file path in your content pack folder to load instead (like 'assets/dinosaur.png'). This can be a .json (data), .png (image), .tbin (map), or .xnb file. This field supports tokens and capitalisation doesn't matter.", + "description": "The relative file path in your content pack folder to load instead (like 'assets/dinosaur.png'). This can be a .json (data), .png (image), .tbin (map), or .xnb file. This field supports tokens and capitalization doesn't matter.", "type": "string", "allOf": [ { @@ -310,7 +310,7 @@ "then": { "properties": { "FromFile": { - "description": "The relative path to the map in your content pack folder from which to copy (like assets/town.tbin). This can be a .tbin or .xnb file. This field supports tokens and capitalisation doesn't matter.\nContent Patcher will handle tilesheets referenced by the FromFile map for you if it's a .tbin file:\n - If a tilesheet isn't referenced by the target map, Content Patcher will add it for you (with a z_ ID prefix to avoid conflicts with hardcoded game logic). If the source map has a custom version of a tilesheet that's already referenced, it'll be added as a separate tilesheet only used by your tiles.\n - If you include the tilesheet file in your mod folder, Content Patcher will use that one automatically; otherwise it will be loaded from the game's Content/Maps folder." + "description": "The relative path to the map in your content pack folder from which to copy (like assets/town.tbin). This can be a .tbin or .xnb file. This field supports tokens and capitalization doesn't matter.\nContent Patcher will handle tilesheets referenced by the FromFile map for you if it's a .tbin file:\n - If a tilesheet isn't referenced by the target map, Content Patcher will add it for you (with a z_ ID prefix to avoid conflicts with hardcoded game logic). If the source map has a custom version of a tilesheet that's already referenced, it'll be added as a separate tilesheet only used by your tiles.\n - If you include the tilesheet file in your mod folder, Content Patcher will use that one automatically; otherwise it will be loaded from the game's Content/Maps folder." }, "FromArea": { "description": "The part of the source map to copy. Defaults to the whole source map." diff --git a/src/SMAPI.sln.DotSettings b/src/SMAPI.sln.DotSettings index 5f67fd9e..556f1ec0 100644 --- a/src/SMAPI.sln.DotSettings +++ b/src/SMAPI.sln.DotSettings @@ -23,4 +23,46 @@ True True True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True \ No newline at end of file diff --git a/src/SMAPI/Context.cs b/src/SMAPI/Context.cs index a933752d..a7238b32 100644 --- a/src/SMAPI/Context.cs +++ b/src/SMAPI/Context.cs @@ -14,10 +14,10 @@ namespace StardewModdingAPI /**** ** Public ****/ - /// Whether the game has performed core initialisation. This becomes true right before the first update tick.. + /// Whether the game has performed core initialization. This becomes true right before the first update tick. public static bool IsGameLaunched { get; internal set; } - /// Whether the player has loaded a save and the world has finished initialising. + /// Whether the player has loaded a save and the world has finished initializing. public static bool IsWorldReady { get; internal set; } /// Whether is true and the player is free to act in the world (no menu is displayed, no cutscene is in progress, etc). diff --git a/src/SMAPI/Enums/LoadStage.cs b/src/SMAPI/Enums/LoadStage.cs index 6ff7de4f..5c2b0412 100644 --- a/src/SMAPI/Enums/LoadStage.cs +++ b/src/SMAPI/Enums/LoadStage.cs @@ -6,10 +6,10 @@ namespace StardewModdingAPI.Enums /// A save is not loaded or loading. None, - /// The game is creating a new save slot, and has initialised the basic save info. + /// The game is creating a new save slot, and has initialized the basic save info. CreatedBasicInfo, - /// The game is creating a new save slot, and has initialised the in-game locations. + /// The game is creating a new save slot, and has initialized the in-game locations. CreatedLocations, /// The game is creating a new save slot, and has created the physical save files. @@ -18,7 +18,7 @@ namespace StardewModdingAPI.Enums /// The game is loading a save slot, and has read the raw save data into . Not applicable when connecting to a multiplayer host. This is equivalent to value 20. SaveParsed, - /// The game is loading a save slot, and has applied the basic save info (including player data). Not applicable when connecting to a multiplayer host. Note that some basic info (like daily luck) is not initialised at this point. This is equivalent to value 36. + /// The game is loading a save slot, and has applied the basic save info (including player data). Not applicable when connecting to a multiplayer host. Note that some basic info (like daily luck) is not initialized at this point. This is equivalent to value 36. SaveLoadedBasicInfo, /// The game is loading a save slot, and has applied the in-game location data. Not applicable when connecting to a multiplayer host. This is equivalent to value 50. @@ -27,10 +27,10 @@ namespace StardewModdingAPI.Enums /// The final metadata has been loaded from the save file. This happens before the game applies problem fixes, checks for achievements, starts music, etc. Not applicable when connecting to a multiplayer host. Preloaded, - /// The save is fully loaded, but the world may not be fully initialised yet. + /// The save is fully loaded, but the world may not be fully initialized yet. Loaded, - /// The save is fully loaded, the world has been initialised, and is now true. + /// The save is fully loaded, the world has been initialized, and is now true. Ready } } diff --git a/src/SMAPI/Events/IGameLoopEvents.cs b/src/SMAPI/Events/IGameLoopEvents.cs index 6fb56c8b..a576895b 100644 --- a/src/SMAPI/Events/IGameLoopEvents.cs +++ b/src/SMAPI/Events/IGameLoopEvents.cs @@ -5,7 +5,7 @@ namespace StardewModdingAPI.Events /// Events linked to the game's update loop. The update loop runs roughly ≈60 times/second to run game logic like state changes, action handling, etc. These can be useful, but you should consider more semantic events like if possible. public interface IGameLoopEvents { - /// Raised after the game is launched, right before the first update tick. This happens once per game session (unrelated to loading saves). All mods are loaded and initialised at this point, so this is a good time to set up mod integrations. + /// Raised after the game is launched, right before the first update tick. This happens once per game session (unrelated to loading saves). All mods are loaded and initialized at this point, so this is a good time to set up mod integrations. event EventHandler GameLaunched; /// Raised before the game state is updated (≈60 times per second). @@ -32,7 +32,7 @@ namespace StardewModdingAPI.Events /// Raised after the game finishes writing data to the save file (except the initial save creation). event EventHandler Saved; - /// Raised after the player loads a save slot and the world is initialised. + /// Raised after the player loads a save slot and the world is initialized. event EventHandler SaveLoaded; /// Raised after the game begins a new day (including when the player loads a save). diff --git a/src/SMAPI/Events/IModEvents.cs b/src/SMAPI/Events/IModEvents.cs index bd7ab880..1f892b31 100644 --- a/src/SMAPI/Events/IModEvents.cs +++ b/src/SMAPI/Events/IModEvents.cs @@ -21,7 +21,7 @@ namespace StardewModdingAPI.Events /// Events raised when something changes in the world. IWorldEvents World { get; } - /// Events serving specialised edge cases that shouldn't be used by most mods. - ISpecialisedEvents Specialised { get; } + /// Events serving specialized edge cases that shouldn't be used by most mods. + ISpecializedEvents Specialized { get; } } } diff --git a/src/SMAPI/Events/ISpecialisedEvents.cs b/src/SMAPI/Events/ISpecialisedEvents.cs index ecb109e6..bf70956d 100644 --- a/src/SMAPI/Events/ISpecialisedEvents.cs +++ b/src/SMAPI/Events/ISpecialisedEvents.cs @@ -2,8 +2,8 @@ using System; namespace StardewModdingAPI.Events { - /// Events serving specialised edge cases that shouldn't be used by most mods. - public interface ISpecialisedEvents + /// Events serving specialized edge cases that shouldn't be used by most mods. + public interface ISpecializedEvents { /// Raised when the low-level stage in the game's loading process has changed. This is an advanced event for mods which need to run code at specific points in the loading process. The available stages or when they happen might change without warning in future versions (e.g. due to changes in the game's load process), so mods using this event are more likely to break or have bugs. Most mods should use instead. event EventHandler LoadStageChanged; diff --git a/src/SMAPI/Events/LoadStageChangedEventArgs.cs b/src/SMAPI/Events/LoadStageChangedEventArgs.cs index e837a5f1..3529dcf3 100644 --- a/src/SMAPI/Events/LoadStageChangedEventArgs.cs +++ b/src/SMAPI/Events/LoadStageChangedEventArgs.cs @@ -3,7 +3,7 @@ using StardewModdingAPI.Enums; namespace StardewModdingAPI.Events { - /// Event arguments for an event. + /// Event arguments for an event. public class LoadStageChangedEventArgs : EventArgs { /********* diff --git a/src/SMAPI/Events/UnvalidatedUpdateTickedEventArgs.cs b/src/SMAPI/Events/UnvalidatedUpdateTickedEventArgs.cs index 13c367a0..258e2f99 100644 --- a/src/SMAPI/Events/UnvalidatedUpdateTickedEventArgs.cs +++ b/src/SMAPI/Events/UnvalidatedUpdateTickedEventArgs.cs @@ -3,7 +3,7 @@ using StardewModdingAPI.Framework; namespace StardewModdingAPI.Events { - /// Event arguments for an event. + /// Event arguments for an event. public class UnvalidatedUpdateTickedEventArgs : EventArgs { /********* diff --git a/src/SMAPI/Events/UnvalidatedUpdateTickingEventArgs.cs b/src/SMAPI/Events/UnvalidatedUpdateTickingEventArgs.cs index c2e60f25..e3c8b3ee 100644 --- a/src/SMAPI/Events/UnvalidatedUpdateTickingEventArgs.cs +++ b/src/SMAPI/Events/UnvalidatedUpdateTickingEventArgs.cs @@ -3,7 +3,7 @@ using StardewModdingAPI.Framework; namespace StardewModdingAPI.Events { - /// Event arguments for an event. + /// Event arguments for an event. public class UnvalidatedUpdateTickingEventArgs : EventArgs { /********* diff --git a/src/SMAPI/Framework/CommandManager.cs b/src/SMAPI/Framework/CommandManager.cs index fdaafff1..ceeb6f93 100644 --- a/src/SMAPI/Framework/CommandManager.cs +++ b/src/SMAPI/Framework/CommandManager.cs @@ -29,7 +29,7 @@ namespace StardewModdingAPI.Framework /// There's already a command with that name. public void Add(IModMetadata mod, string name, string documentation, Action callback, bool allowNullCallback = false) { - name = this.GetNormalisedName(name); + name = this.GetNormalizedName(name); // validate format if (string.IsNullOrWhiteSpace(name)) @@ -52,7 +52,7 @@ namespace StardewModdingAPI.Framework /// Returns the matching command, or null if not found. public Command Get(string name) { - name = this.GetNormalisedName(name); + name = this.GetNormalizedName(name); this.Commands.TryGetValue(name, out Command command); return command; } @@ -84,7 +84,7 @@ namespace StardewModdingAPI.Framework // parse input args = this.ParseArgs(input); - name = this.GetNormalisedName(args[0]); + name = this.GetNormalizedName(args[0]); args = args.Skip(1).ToArray(); // get command @@ -97,8 +97,8 @@ namespace StardewModdingAPI.Framework /// Returns whether a matching command was triggered. public bool Trigger(string name, string[] arguments) { - // get normalised name - name = this.GetNormalisedName(name); + // get normalized name + name = this.GetNormalizedName(name); if (name == null) return false; @@ -140,9 +140,9 @@ namespace StardewModdingAPI.Framework return args.Where(item => !string.IsNullOrWhiteSpace(item)).ToArray(); } - /// Get a normalised command name. + /// Get a normalized command name. /// The command name. - private string GetNormalisedName(string name) + private string GetNormalizedName(string name) { name = name?.Trim().ToLower(); return !string.IsNullOrWhiteSpace(name) diff --git a/src/SMAPI/Framework/Content/AssetData.cs b/src/SMAPI/Framework/Content/AssetData.cs index 553404d3..cacc6078 100644 --- a/src/SMAPI/Framework/Content/AssetData.cs +++ b/src/SMAPI/Framework/Content/AssetData.cs @@ -24,13 +24,13 @@ namespace StardewModdingAPI.Framework.Content ** Public methods *********/ /// Construct an instance. - /// The content's locale code, if the content is localised. - /// The normalised asset name being read. + /// The content's locale code, if the content is localized. + /// The normalized asset name being read. /// The content data being read. - /// Normalises an asset key to match the cache key. + /// Normalizes an asset key to match the cache key. /// A callback to invoke when the data is replaced (if any). - public AssetData(string locale, string assetName, TValue data, Func getNormalisedPath, Action onDataReplaced) - : base(locale, assetName, data.GetType(), getNormalisedPath) + public AssetData(string locale, string assetName, TValue data, Func getNormalizedPath, Action onDataReplaced) + : base(locale, assetName, data.GetType(), getNormalizedPath) { this.Data = data; this.OnDataReplaced = onDataReplaced; diff --git a/src/SMAPI/Framework/Content/AssetDataForDictionary.cs b/src/SMAPI/Framework/Content/AssetDataForDictionary.cs index a331f38a..26cbff5a 100644 --- a/src/SMAPI/Framework/Content/AssetDataForDictionary.cs +++ b/src/SMAPI/Framework/Content/AssetDataForDictionary.cs @@ -10,12 +10,12 @@ namespace StardewModdingAPI.Framework.Content ** Public methods *********/ /// Construct an instance. - /// The content's locale code, if the content is localised. - /// The normalised asset name being read. + /// The content's locale code, if the content is localized. + /// The normalized asset name being read. /// The content data being read. - /// Normalises an asset key to match the cache key. + /// Normalizes an asset key to match the cache key. /// A callback to invoke when the data is replaced (if any). - public AssetDataForDictionary(string locale, string assetName, IDictionary data, Func getNormalisedPath, Action> onDataReplaced) - : base(locale, assetName, data, getNormalisedPath, onDataReplaced) { } + public AssetDataForDictionary(string locale, string assetName, IDictionary data, Func getNormalizedPath, Action> onDataReplaced) + : base(locale, assetName, data, getNormalizedPath, onDataReplaced) { } } } diff --git a/src/SMAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs index f2d21b5e..4ae2ad68 100644 --- a/src/SMAPI/Framework/Content/AssetDataForImage.cs +++ b/src/SMAPI/Framework/Content/AssetDataForImage.cs @@ -19,13 +19,13 @@ namespace StardewModdingAPI.Framework.Content ** Public methods *********/ /// Construct an instance. - /// The content's locale code, if the content is localised. - /// The normalised asset name being read. + /// The content's locale code, if the content is localized. + /// The normalized asset name being read. /// The content data being read. - /// Normalises an asset key to match the cache key. + /// Normalizes an asset key to match the cache key. /// A callback to invoke when the data is replaced (if any). - public AssetDataForImage(string locale, string assetName, Texture2D data, Func getNormalisedPath, Action onDataReplaced) - : base(locale, assetName, data, getNormalisedPath, onDataReplaced) { } + public AssetDataForImage(string locale, string assetName, Texture2D data, Func getNormalizedPath, Action onDataReplaced) + : base(locale, assetName, data, getNormalizedPath, onDataReplaced) { } /// Overwrite part of the image. /// The image to patch into the content. diff --git a/src/SMAPI/Framework/Content/AssetDataForObject.cs b/src/SMAPI/Framework/Content/AssetDataForObject.cs index 90f9e2d4..4dbc988c 100644 --- a/src/SMAPI/Framework/Content/AssetDataForObject.cs +++ b/src/SMAPI/Framework/Content/AssetDataForObject.cs @@ -11,19 +11,19 @@ namespace StardewModdingAPI.Framework.Content ** Public methods *********/ /// Construct an instance. - /// The content's locale code, if the content is localised. - /// The normalised asset name being read. + /// The content's locale code, if the content is localized. + /// The normalized asset name being read. /// The content data being read. - /// Normalises an asset key to match the cache key. - public AssetDataForObject(string locale, string assetName, object data, Func getNormalisedPath) - : base(locale, assetName, data, getNormalisedPath, onDataReplaced: null) { } + /// Normalizes an asset key to match the cache key. + public AssetDataForObject(string locale, string assetName, object data, Func getNormalizedPath) + : base(locale, assetName, data, getNormalizedPath, onDataReplaced: null) { } /// Construct an instance. /// The asset metadata. /// The content data being read. - /// Normalises an asset key to match the cache key. - public AssetDataForObject(IAssetInfo info, object data, Func getNormalisedPath) - : this(info.Locale, info.AssetName, data, getNormalisedPath) { } + /// Normalizes an asset key to match the cache key. + public AssetDataForObject(IAssetInfo info, object data, Func getNormalizedPath) + : this(info.Locale, info.AssetName, data, getNormalizedPath) { } /// Get a helper to manipulate the data as a dictionary. /// The expected dictionary key. @@ -31,14 +31,14 @@ namespace StardewModdingAPI.Framework.Content /// The content being read isn't a dictionary. public IAssetDataForDictionary AsDictionary() { - return new AssetDataForDictionary(this.Locale, this.AssetName, this.GetData>(), this.GetNormalisedPath, this.ReplaceWith); + return new AssetDataForDictionary(this.Locale, this.AssetName, this.GetData>(), this.GetNormalizedPath, this.ReplaceWith); } /// Get a helper to manipulate the data as an image. /// The content being read isn't an image. public IAssetDataForImage AsImage() { - return new AssetDataForImage(this.Locale, this.AssetName, this.GetData(), this.GetNormalisedPath, this.ReplaceWith); + return new AssetDataForImage(this.Locale, this.AssetName, this.GetData(), this.GetNormalizedPath, this.ReplaceWith); } /// Get the data as a given type. diff --git a/src/SMAPI/Framework/Content/AssetInfo.cs b/src/SMAPI/Framework/Content/AssetInfo.cs index e5211290..9b685e72 100644 --- a/src/SMAPI/Framework/Content/AssetInfo.cs +++ b/src/SMAPI/Framework/Content/AssetInfo.cs @@ -9,17 +9,17 @@ namespace StardewModdingAPI.Framework.Content /********* ** Fields *********/ - /// Normalises an asset key to match the cache key. - protected readonly Func GetNormalisedPath; + /// Normalizes an asset key to match the cache key. + protected readonly Func GetNormalizedPath; /********* ** Accessors *********/ - /// The content's locale code, if the content is localised. + /// The content's locale code, if the content is localized. public string Locale { get; } - /// The normalised asset name being read. The format may change between platforms; see to compare with a known path. + /// The normalized asset name being read. The format may change between platforms; see to compare with a known path. public string AssetName { get; } /// The content data type. @@ -30,23 +30,23 @@ namespace StardewModdingAPI.Framework.Content ** Public methods *********/ /// Construct an instance. - /// The content's locale code, if the content is localised. - /// The normalised asset name being read. + /// The content's locale code, if the content is localized. + /// The normalized asset name being read. /// The content type being read. - /// Normalises an asset key to match the cache key. - public AssetInfo(string locale, string assetName, Type type, Func getNormalisedPath) + /// Normalizes an asset key to match the cache key. + public AssetInfo(string locale, string assetName, Type type, Func getNormalizedPath) { this.Locale = locale; this.AssetName = assetName; this.DataType = type; - this.GetNormalisedPath = getNormalisedPath; + this.GetNormalizedPath = getNormalizedPath; } - /// Get whether the asset name being loaded matches a given name after normalisation. + /// Get whether the asset name being loaded matches a given name after normalization. /// The expected asset path, relative to the game's content folder and without the .xnb extension or locale suffix (like 'Data\ObjectInformation'). public bool AssetNameEquals(string path) { - path = this.GetNormalisedPath(path); + path = this.GetNormalizedPath(path); return this.AssetName.Equals(path, StringComparison.InvariantCultureIgnoreCase); } diff --git a/src/SMAPI/Framework/Content/ContentCache.cs b/src/SMAPI/Framework/Content/ContentCache.cs index 55a96ed2..4178b663 100644 --- a/src/SMAPI/Framework/Content/ContentCache.cs +++ b/src/SMAPI/Framework/Content/ContentCache.cs @@ -10,7 +10,7 @@ using StardewValley; namespace StardewModdingAPI.Framework.Content { - /// A low-level wrapper around the content cache which handles reading, writing, and invalidating entries in the cache. This doesn't handle any higher-level logic like localisation, loading content, etc. It assumes all keys passed in are already normalised. + /// A low-level wrapper around the content cache which handles reading, writing, and invalidating entries in the cache. This doesn't handle any higher-level logic like localization, loading content, etc. It assumes all keys passed in are already normalized. internal class ContentCache { /********* @@ -19,8 +19,8 @@ namespace StardewModdingAPI.Framework.Content /// The underlying asset cache. private readonly IDictionary Cache; - /// Applies platform-specific asset key normalisation so it's consistent with the underlying cache. - private readonly Func NormaliseAssetNameForPlatform; + /// Applies platform-specific asset key normalization so it's consistent with the underlying cache. + private readonly Func NormalizeAssetNameForPlatform; /********* @@ -52,14 +52,14 @@ namespace StardewModdingAPI.Framework.Content // init this.Cache = reflection.GetField>(contentManager, "loadedAssets").GetValue(); - // get key normalisation logic + // get key normalization logic if (Constants.Platform == Platform.Windows) { IReflectedMethod method = reflection.GetMethod(typeof(TitleContainer), "GetCleanPath"); - this.NormaliseAssetNameForPlatform = path => method.Invoke(path); + this.NormalizeAssetNameForPlatform = path => method.Invoke(path); } else - this.NormaliseAssetNameForPlatform = key => key.Replace('\\', '/'); // based on MonoGame's ContentManager.Load logic + this.NormalizeAssetNameForPlatform = key => key.Replace('\\', '/'); // based on MonoGame's ContentManager.Load logic } /**** @@ -74,25 +74,25 @@ namespace StardewModdingAPI.Framework.Content /**** - ** Normalise + ** Normalize ****/ - /// Normalise path separators in a file path. For asset keys, see instead. - /// The file path to normalise. + /// Normalize path separators in a file path. For asset keys, see instead. + /// The file path to normalize. [Pure] - public string NormalisePathSeparators(string path) + public string NormalizePathSeparators(string path) { - return PathUtilities.NormalisePathSeparators(path); + return PathUtilities.NormalizePathSeparators(path); } - /// Normalise a cache key so it's consistent with the underlying cache. + /// Normalize a cache key so it's consistent with the underlying cache. /// The asset key. [Pure] - public string NormaliseKey(string key) + public string NormalizeKey(string key) { - key = this.NormalisePathSeparators(key); + key = this.NormalizePathSeparators(key); return key.EndsWith(".xnb", StringComparison.InvariantCultureIgnoreCase) ? key.Substring(0, key.Length - 4) - : this.NormaliseAssetNameForPlatform(key); + : this.NormalizeAssetNameForPlatform(key); } /**** diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index 39bebdd1..08ebe6a5 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -9,7 +9,7 @@ using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.ContentManagers; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Metadata; -using StardewModdingAPI.Toolkit.Serialisation; +using StardewModdingAPI.Toolkit.Serialization; using StardewModdingAPI.Toolkit.Utilities; using StardewValley; @@ -74,7 +74,7 @@ namespace StardewModdingAPI.Framework /// Construct an instance. /// The service provider to use to locate services. /// The root directory to search for content. - /// The current culture for which to localise content. + /// The current culture for which to localize content. /// Encapsulates monitoring and logging. /// Simplifies access to private code. /// Encapsulates SMAPI's JSON file parsing. @@ -89,7 +89,7 @@ namespace StardewModdingAPI.Framework this.ContentManagers.Add( this.MainContentManager = new GameContentManager("Game1.content", serviceProvider, rootDirectory, currentCulture, this, monitor, reflection, this.OnDisposing, onLoadingFirstAsset) ); - this.CoreAssets = new CoreAssetPropagator(this.MainContentManager.AssertAndNormaliseAssetName, reflection, monitor); + this.CoreAssets = new CoreAssetPropagator(this.MainContentManager.AssertAndNormalizeAssetName, reflection, monitor); } /// Get a new content manager which handles reading files from the game content folder with support for interception. @@ -250,7 +250,7 @@ namespace StardewModdingAPI.Framework string locale = this.GetLocale(); return this.InvalidateCache((assetName, type) => { - IAssetInfo info = new AssetInfo(locale, assetName, type, this.MainContentManager.AssertAndNormaliseAssetName); + IAssetInfo info = new AssetInfo(locale, assetName, type, this.MainContentManager.AssertAndNormalizeAssetName); return predicate(info); }, dispose); } diff --git a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs index fc558eb9..de39dbae 100644 --- a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs @@ -64,7 +64,7 @@ namespace StardewModdingAPI.Framework.ContentManagers /// A name for the mod manager. Not guaranteed to be unique. /// The service provider to use to locate services. /// The root directory to search for content. - /// The current culture for which to localise content. + /// The current culture for which to localize content. /// The central coordinator which manages content managers. /// Encapsulates monitoring and logging. /// Simplifies access to private code. @@ -109,7 +109,7 @@ namespace StardewModdingAPI.Framework.ContentManagers /// Whether to read/write the loaded asset to the asset cache. public abstract T Load(string assetName, LocalizedContentManager.LanguageCode language, bool useCache); - /// Load the base asset without localisation. + /// Load the base asset without localization. /// The type of asset to load. /// The asset path relative to the loader root directory, not including the .xnb extension. [Obsolete("This method is implemented for the base game and should not be used directly. To load an asset from the underlying content manager directly, use " + nameof(BaseContentManager.RawLoad) + " instead.")] @@ -121,19 +121,19 @@ namespace StardewModdingAPI.Framework.ContentManagers /// Perform any cleanup needed when the locale changes. public virtual void OnLocaleChanged() { } - /// Normalise path separators in a file path. For asset keys, see instead. - /// The file path to normalise. + /// Normalize path separators in a file path. For asset keys, see instead. + /// The file path to normalize. [Pure] - public string NormalisePathSeparators(string path) + public string NormalizePathSeparators(string path) { - return this.Cache.NormalisePathSeparators(path); + return this.Cache.NormalizePathSeparators(path); } - /// Assert that the given key has a valid format and return a normalised form consistent with the underlying cache. + /// Assert that the given key has a valid format and return a normalized form consistent with the underlying cache. /// The asset key to check. /// The asset key is empty or contains invalid characters. [SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local", Justification = "Parameter is only used for assertion checks by design.")] - public string AssertAndNormaliseAssetName(string assetName) + public string AssertAndNormalizeAssetName(string assetName) { // NOTE: the game checks for ContentLoadException to handle invalid keys, so avoid // throwing other types like ArgumentException here. @@ -142,7 +142,7 @@ namespace StardewModdingAPI.Framework.ContentManagers if (assetName.Intersect(Path.GetInvalidPathChars()).Any()) throw new SContentLoadException("The asset key or local path contains invalid characters."); - return this.Cache.NormaliseKey(assetName); + return this.Cache.NormalizeKey(assetName); } /**** @@ -165,8 +165,8 @@ namespace StardewModdingAPI.Framework.ContentManagers /// The asset path relative to the loader root directory, not including the .xnb extension. public bool IsLoaded(string assetName) { - assetName = this.Cache.NormaliseKey(assetName); - return this.IsNormalisedKeyLoaded(assetName); + assetName = this.Cache.NormalizeKey(assetName); + return this.IsNormalizedKeyLoaded(assetName); } /// Get the cached asset keys. @@ -248,7 +248,7 @@ namespace StardewModdingAPI.Framework.ContentManagers *********/ /// Load an asset file directly from the underlying content manager. /// The type of asset to load. - /// The normalised asset key. + /// The normalized asset key. /// Whether to read/write the loaded asset to the asset cache. protected virtual T RawLoad(string assetName, bool useCache) { @@ -264,17 +264,17 @@ namespace StardewModdingAPI.Framework.ContentManagers /// The language code for which to inject the asset. protected virtual void Inject(string assetName, T value, LanguageCode language) { - assetName = this.AssertAndNormaliseAssetName(assetName); + assetName = this.AssertAndNormalizeAssetName(assetName); this.Cache[assetName] = value; } /// Parse a cache key into its component parts. /// The input cache key. /// The original asset name. - /// The asset locale code (or null if not localised). + /// The asset locale code (or null if not localized). protected void ParseCacheKey(string cacheKey, out string assetName, out string localeCode) { - // handle localised key + // handle localized key if (!string.IsNullOrWhiteSpace(cacheKey)) { int lastSepIndex = cacheKey.LastIndexOf(".", StringComparison.InvariantCulture); @@ -296,8 +296,8 @@ namespace StardewModdingAPI.Framework.ContentManagers } /// Get whether an asset has already been loaded. - /// The normalised asset name. - protected abstract bool IsNormalisedKeyLoaded(string normalisedAssetName); + /// The normalized asset name. + protected abstract bool IsNormalizedKeyLoaded(string normalizedAssetName); /// Get the locale codes (like ja-JP) used in asset keys. private IDictionary GetKeyLocales() diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index 488ec245..c64e9ba9 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -26,8 +26,8 @@ namespace StardewModdingAPI.Framework.ContentManagers /// Interceptors which edit matching assets after they're loaded. private IDictionary> Editors => this.Coordinator.Editors; - /// A lookup which indicates whether the asset is localisable (i.e. the filename contains the locale), if previously loaded. - private readonly IDictionary IsLocalisableLookup; + /// A lookup which indicates whether the asset is localizable (i.e. the filename contains the locale), if previously loaded. + private readonly IDictionary IsLocalizableLookup; /// Whether the next load is the first for any game content manager. private static bool IsFirstLoad = true; @@ -43,7 +43,7 @@ namespace StardewModdingAPI.Framework.ContentManagers /// A name for the mod manager. Not guaranteed to be unique. /// The service provider to use to locate services. /// The root directory to search for content. - /// The current culture for which to localise content. + /// The current culture for which to localize content. /// The central coordinator which manages content managers. /// Encapsulates monitoring and logging. /// Simplifies access to private code. @@ -52,7 +52,7 @@ namespace StardewModdingAPI.Framework.ContentManagers public GameContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, Action onDisposing, Action onLoadingFirstAsset) : base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isNamespaced: false) { - this.IsLocalisableLookup = reflection.GetField>(this, "_localizedAsset").GetValue(); + this.IsLocalizableLookup = reflection.GetField>(this, "_localizedAsset").GetValue(); this.OnLoadingFirstAsset = onLoadingFirstAsset; } @@ -70,8 +70,8 @@ namespace StardewModdingAPI.Framework.ContentManagers this.OnLoadingFirstAsset(); } - // normalise asset name - assetName = this.AssertAndNormaliseAssetName(assetName); + // normalize asset name + assetName = this.AssertAndNormalizeAssetName(assetName); if (this.TryParseExplicitLanguageAssetKey(assetName, out string newAssetName, out LanguageCode newLanguage)) return this.Load(newAssetName, newLanguage, useCache); @@ -101,10 +101,10 @@ namespace StardewModdingAPI.Framework.ContentManagers data = this.AssetsBeingLoaded.Track(assetName, () => { string locale = this.GetLocale(language); - IAssetInfo info = new AssetInfo(locale, assetName, typeof(T), this.AssertAndNormaliseAssetName); + IAssetInfo info = new AssetInfo(locale, assetName, typeof(T), this.AssertAndNormalizeAssetName); IAssetData asset = this.ApplyLoader(info) - ?? new AssetDataForObject(info, this.RawLoad(assetName, language, useCache), this.AssertAndNormaliseAssetName); + ?? new AssetDataForObject(info, this.RawLoad(assetName, language, useCache), this.AssertAndNormalizeAssetName); asset = this.ApplyEditors(info, asset); return (T)asset.Data; }); @@ -122,7 +122,7 @@ namespace StardewModdingAPI.Framework.ContentManagers // find assets for which a translatable version was loaded HashSet removeAssetNames = new HashSet(StringComparer.InvariantCultureIgnoreCase); - foreach (string key in this.IsLocalisableLookup.Where(p => p.Value).Select(p => p.Key)) + foreach (string key in this.IsLocalizableLookup.Where(p => p.Value).Select(p => p.Key)) removeAssetNames.Add(this.TryParseExplicitLanguageAssetKey(key, out string assetName, out _) ? assetName : key); // invalidate translatable assets @@ -149,20 +149,20 @@ namespace StardewModdingAPI.Framework.ContentManagers ** Private methods *********/ /// Get whether an asset has already been loaded. - /// The normalised asset name. - protected override bool IsNormalisedKeyLoaded(string normalisedAssetName) + /// The normalized asset name. + protected override bool IsNormalizedKeyLoaded(string normalizedAssetName) { // default English - if (this.Language == LocalizedContentManager.LanguageCode.en || this.Coordinator.IsManagedAssetKey(normalisedAssetName)) - return this.Cache.ContainsKey(normalisedAssetName); + if (this.Language == LocalizedContentManager.LanguageCode.en || this.Coordinator.IsManagedAssetKey(normalizedAssetName)) + return this.Cache.ContainsKey(normalizedAssetName); // translated - string keyWithLocale = $"{normalisedAssetName}.{this.GetLocale(this.GetCurrentLanguage())}"; - if (this.IsLocalisableLookup.TryGetValue(keyWithLocale, out bool localisable)) + string keyWithLocale = $"{normalizedAssetName}.{this.GetLocale(this.GetCurrentLanguage())}"; + if (this.IsLocalizableLookup.TryGetValue(keyWithLocale, out bool localizable)) { - return localisable + return localizable ? this.Cache.ContainsKey(keyWithLocale) - : this.Cache.ContainsKey(normalisedAssetName); + : this.Cache.ContainsKey(normalizedAssetName); } // not loaded yet @@ -190,13 +190,13 @@ namespace StardewModdingAPI.Framework.ContentManagers string keyWithLocale = $"{assetName}.{this.GetLocale(language)}"; if (this.Cache.ContainsKey(keyWithLocale)) { - this.IsLocalisableLookup[assetName] = true; - this.IsLocalisableLookup[keyWithLocale] = true; + this.IsLocalizableLookup[assetName] = true; + this.IsLocalizableLookup[keyWithLocale] = true; } else if (this.Cache.ContainsKey(assetName)) { - this.IsLocalisableLookup[assetName] = false; - this.IsLocalisableLookup[keyWithLocale] = false; + this.IsLocalizableLookup[assetName] = false; + this.IsLocalizableLookup[keyWithLocale] = false; } else this.Monitor.Log($"Asset '{assetName}' could not be found in the cache immediately after injection.", LogLevel.Error); @@ -204,7 +204,7 @@ namespace StardewModdingAPI.Framework.ContentManagers /// Load an asset file directly from the underlying content manager. /// The type of asset to load. - /// The normalised asset key. + /// The normalized asset key. /// The language code for which to load content. /// Whether to read/write the loaded asset to the asset cache. /// Derived from . @@ -214,19 +214,19 @@ namespace StardewModdingAPI.Framework.ContentManagers if (language != LocalizedContentManager.LanguageCode.en) { string translatedKey = $"{assetName}.{this.GetLocale(language)}"; - if (!this.IsLocalisableLookup.TryGetValue(translatedKey, out bool isTranslatable) || isTranslatable) + if (!this.IsLocalizableLookup.TryGetValue(translatedKey, out bool isTranslatable) || isTranslatable) { try { T obj = base.RawLoad(translatedKey, useCache); - this.IsLocalisableLookup[assetName] = true; - this.IsLocalisableLookup[translatedKey] = true; + this.IsLocalizableLookup[assetName] = true; + this.IsLocalizableLookup[translatedKey] = true; return obj; } catch (ContentLoadException) { - this.IsLocalisableLookup[assetName] = false; - this.IsLocalisableLookup[translatedKey] = false; + this.IsLocalizableLookup[assetName] = false; + this.IsLocalizableLookup[translatedKey] = false; } } } @@ -313,7 +313,7 @@ namespace StardewModdingAPI.Framework.ContentManagers } // return matched asset - return new AssetDataForObject(info, data, this.AssertAndNormaliseAssetName); + return new AssetDataForObject(info, data, this.AssertAndNormalizeAssetName); } /// Apply any to a loaded asset. @@ -322,7 +322,7 @@ namespace StardewModdingAPI.Framework.ContentManagers /// The loaded asset. private IAssetData ApplyEditors(IAssetInfo info, IAssetData asset) { - IAssetData GetNewData(object data) => new AssetDataForObject(info, data, this.AssertAndNormaliseAssetName); + IAssetData GetNewData(object data) => new AssetDataForObject(info, data, this.AssertAndNormalizeAssetName); // edit asset foreach (var entry in this.GetInterceptors(this.Editors)) diff --git a/src/SMAPI/Framework/ContentManagers/IContentManager.cs b/src/SMAPI/Framework/ContentManagers/IContentManager.cs index 78211821..12c01352 100644 --- a/src/SMAPI/Framework/ContentManagers/IContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/IContentManager.cs @@ -39,15 +39,15 @@ namespace StardewModdingAPI.Framework.ContentManagers /// Perform any cleanup needed when the locale changes. void OnLocaleChanged(); - /// Normalise path separators in a file path. For asset keys, see instead. - /// The file path to normalise. + /// Normalize path separators in a file path. For asset keys, see instead. + /// The file path to normalize. [Pure] - string NormalisePathSeparators(string path); + string NormalizePathSeparators(string path); - /// Assert that the given key has a valid format and return a normalised form consistent with the underlying cache. + /// Assert that the given key has a valid format and return a normalized form consistent with the underlying cache. /// The asset key to check. /// The asset key is empty or contains invalid characters. - string AssertAndNormaliseAssetName(string assetName); + string AssertAndNormalizeAssetName(string assetName); /// Get the current content locale. string GetLocale(); diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index 34cabefc..b88bd71e 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -7,7 +7,7 @@ using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.Reflection; -using StardewModdingAPI.Toolkit.Serialisation; +using StardewModdingAPI.Toolkit.Serialization; using StardewModdingAPI.Toolkit.Utilities; using StardewValley; using xTile; @@ -41,7 +41,7 @@ namespace StardewModdingAPI.Framework.ContentManagers /// The game content manager used for map tilesheets not provided by the mod. /// The service provider to use to locate services. /// The root directory to search for content. - /// The current culture for which to localise content. + /// The current culture for which to localize content. /// The central coordinator which manages content managers. /// Encapsulates monitoring and logging. /// Simplifies access to private code. @@ -78,7 +78,7 @@ namespace StardewModdingAPI.Framework.ContentManagers /// Whether to read/write the loaded asset to the asset cache. public override T Load(string assetName, LanguageCode language, bool useCache) { - assetName = this.AssertAndNormaliseAssetName(assetName); + assetName = this.AssertAndNormalizeAssetName(assetName); // disable caching // This is necessary to avoid assets being shared between content managers, which can @@ -91,7 +91,7 @@ namespace StardewModdingAPI.Framework.ContentManagers // disable language handling // Mod files don't support automatic translation logic, so this should never happen. if (language != this.DefaultLanguage) - throw new InvalidOperationException("Localised assets aren't supported by the mod content manager."); + throw new InvalidOperationException("Localized assets aren't supported by the mod content manager."); // resolve managed asset key { @@ -121,7 +121,7 @@ namespace StardewModdingAPI.Framework.ContentManagers T data = this.RawLoad(assetName, useCache: false); if (data is Map map) { - this.NormaliseTilesheetPaths(map); + this.NormalizeTilesheetPaths(map); this.FixCustomTilesheetPaths(map, relativeMapPath: assetName); } return data; @@ -161,7 +161,7 @@ namespace StardewModdingAPI.Framework.ContentManagers // fetch & cache FormatManager formatManager = FormatManager.Instance; Map map = formatManager.LoadMap(file.FullName); - this.NormaliseTilesheetPaths(map); + this.NormalizeTilesheetPaths(map); this.FixCustomTilesheetPaths(map, relativeMapPath: assetName); return (T)(object)map; } @@ -199,10 +199,10 @@ namespace StardewModdingAPI.Framework.ContentManagers ** Private methods *********/ /// Get whether an asset has already been loaded. - /// The normalised asset name. - protected override bool IsNormalisedKeyLoaded(string normalisedAssetName) + /// The normalized asset name. + protected override bool IsNormalizedKeyLoaded(string normalizedAssetName) { - return this.Cache.ContainsKey(normalisedAssetName); + return this.Cache.ContainsKey(normalizedAssetName); } /// Get a file from the mod folder. @@ -245,12 +245,12 @@ namespace StardewModdingAPI.Framework.ContentManagers return texture; } - /// Normalise map tilesheet paths for the current platform. + /// Normalize map tilesheet paths for the current platform. /// The map whose tilesheets to fix. - private void NormaliseTilesheetPaths(Map map) + private void NormalizeTilesheetPaths(Map map) { foreach (TileSheet tilesheet in map.TileSheets) - tilesheet.ImageSource = this.NormalisePathSeparators(tilesheet.ImageSource); + tilesheet.ImageSource = this.NormalizePathSeparators(tilesheet.ImageSource); } /// Fix custom map tilesheet paths so they can be found by the content manager. @@ -258,7 +258,7 @@ namespace StardewModdingAPI.Framework.ContentManagers /// The relative map path within the mod folder. /// A map tilesheet couldn't be resolved. /// - /// The game's logic for tilesheets in is a bit specialised. It boils + /// The game's logic for tilesheets in is a bit specialized. It boils /// down to this: /// * If the location is indoors or the desert, or the image source contains 'path' or 'object', it's loaded /// as-is relative to the Content folder. @@ -276,7 +276,7 @@ namespace StardewModdingAPI.Framework.ContentManagers // get map info if (!map.TileSheets.Any()) return; - relativeMapPath = this.AssertAndNormaliseAssetName(relativeMapPath); // Mono's Path.GetDirectoryName doesn't handle Windows dir separators + relativeMapPath = this.AssertAndNormalizeAssetName(relativeMapPath); // Mono's Path.GetDirectoryName doesn't handle Windows dir separators string relativeMapFolder = Path.GetDirectoryName(relativeMapPath) ?? ""; // folder path containing the map, relative to the mod folder bool isOutdoors = map.Properties.TryGetValue("Outdoors", out PropertyValue outdoorsProperty) && outdoorsProperty != null; diff --git a/src/SMAPI/Framework/ContentPack.cs b/src/SMAPI/Framework/ContentPack.cs index 829a7dc1..9c0bb9d1 100644 --- a/src/SMAPI/Framework/ContentPack.cs +++ b/src/SMAPI/Framework/ContentPack.cs @@ -2,7 +2,7 @@ using System; using System.IO; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; -using StardewModdingAPI.Toolkit.Serialisation; +using StardewModdingAPI.Toolkit.Serialization; using StardewModdingAPI.Toolkit.Utilities; using xTile; @@ -63,14 +63,14 @@ namespace StardewModdingAPI.Framework /// Read a JSON file from the content pack folder. /// The model type. - /// The file path relative to the contnet directory. - /// Returns the deserialised model, or null if the file doesn't exist or is empty. + /// The file path relative to the content directory. + /// Returns the deserialized model, or null if the file doesn't exist or is empty. /// The is not relative or contains directory climbing (../). public TModel ReadJsonFile(string path) where TModel : class { this.AssertRelativePath(path, nameof(this.ReadJsonFile)); - path = Path.Combine(this.DirectoryPath, PathUtilities.NormalisePathSeparators(path)); + path = Path.Combine(this.DirectoryPath, PathUtilities.NormalizePathSeparators(path)); return this.JsonHelper.ReadJsonFileIfExists(path, out TModel model) ? model : null; @@ -85,7 +85,7 @@ namespace StardewModdingAPI.Framework { this.AssertRelativePath(path, nameof(this.WriteJsonFile)); - path = Path.Combine(this.DirectoryPath, PathUtilities.NormalisePathSeparators(path)); + path = Path.Combine(this.DirectoryPath, PathUtilities.NormalizePathSeparators(path)); this.JsonHelper.WriteJsonFile(path, data); } diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 23879f1d..18b00f69 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -73,7 +73,7 @@ namespace StardewModdingAPI.Framework.Events /// Raised after the game finishes writing data to the save file (except the initial save creation). public readonly ManagedEvent Saved; - /// Raised after the player loads a save slot and the world is initialised. + /// Raised after the player loads a save slot and the world is initialized. public readonly ManagedEvent SaveLoaded; /// Raised after the game begins a new day, including when loading a save. @@ -152,15 +152,15 @@ namespace StardewModdingAPI.Framework.Events public readonly ManagedEvent TerrainFeatureListChanged; /**** - ** Specialised + ** Specialized ****/ - /// Raised when the low-level stage in the game's loading process has changed. See notes on . + /// Raised when the low-level stage in the game's loading process has changed. See notes on . public readonly ManagedEvent LoadStageChanged; - /// Raised before the game performs its overall update tick (≈60 times per second). See notes on . + /// Raised before the game performs its overall update tick (≈60 times per second). See notes on . public readonly ManagedEvent UnvalidatedUpdateTicking; - /// Raised after the game performs its overall update tick (≈60 times per second). See notes on . + /// Raised after the game performs its overall update tick (≈60 times per second). See notes on . public readonly ManagedEvent UnvalidatedUpdateTicked; @@ -172,7 +172,7 @@ namespace StardewModdingAPI.Framework.Events /// The mod registry with which to identify mods. public EventManager(IMonitor monitor, ModRegistry modRegistry) { - // create shortcut initialisers + // create shortcut initializers ManagedEvent ManageEventOf(string typeName, string eventName) => new ManagedEvent($"{typeName}.{eventName}", monitor, modRegistry); // init events (new) @@ -223,9 +223,9 @@ namespace StardewModdingAPI.Framework.Events this.ObjectListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.ObjectListChanged)); this.TerrainFeatureListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.TerrainFeatureListChanged)); - this.LoadStageChanged = ManageEventOf(nameof(IModEvents.Specialised), nameof(ISpecialisedEvents.LoadStageChanged)); - this.UnvalidatedUpdateTicking = ManageEventOf(nameof(IModEvents.Specialised), nameof(ISpecialisedEvents.UnvalidatedUpdateTicking)); - this.UnvalidatedUpdateTicked = ManageEventOf(nameof(IModEvents.Specialised), nameof(ISpecialisedEvents.UnvalidatedUpdateTicked)); + this.LoadStageChanged = ManageEventOf(nameof(IModEvents.Specialized), nameof(ISpecializedEvents.LoadStageChanged)); + this.UnvalidatedUpdateTicking = ManageEventOf(nameof(IModEvents.Specialized), nameof(ISpecializedEvents.UnvalidatedUpdateTicking)); + this.UnvalidatedUpdateTicked = ManageEventOf(nameof(IModEvents.Specialized), nameof(ISpecializedEvents.UnvalidatedUpdateTicked)); } } } diff --git a/src/SMAPI/Framework/Events/ModEvents.cs b/src/SMAPI/Framework/Events/ModEvents.cs index 8ad3936c..1d1c92c6 100644 --- a/src/SMAPI/Framework/Events/ModEvents.cs +++ b/src/SMAPI/Framework/Events/ModEvents.cs @@ -26,8 +26,8 @@ namespace StardewModdingAPI.Framework.Events /// Events raised when something changes in the world. public IWorldEvents World { get; } - /// Events serving specialised edge cases that shouldn't be used by most mods. - public ISpecialisedEvents Specialised { get; } + /// Events serving specialized edge cases that shouldn't be used by most mods. + public ISpecializedEvents Specialized { get; } /********* @@ -44,7 +44,7 @@ namespace StardewModdingAPI.Framework.Events this.Multiplayer = new ModMultiplayerEvents(mod, eventManager); this.Player = new ModPlayerEvents(mod, eventManager); this.World = new ModWorldEvents(mod, eventManager); - this.Specialised = new ModSpecialisedEvents(mod, eventManager); + this.Specialized = new ModSpecializedEvents(mod, eventManager); } } } diff --git a/src/SMAPI/Framework/Events/ModGameLoopEvents.cs b/src/SMAPI/Framework/Events/ModGameLoopEvents.cs index 0177c22e..c15460fa 100644 --- a/src/SMAPI/Framework/Events/ModGameLoopEvents.cs +++ b/src/SMAPI/Framework/Events/ModGameLoopEvents.cs @@ -72,7 +72,7 @@ namespace StardewModdingAPI.Framework.Events remove => this.EventManager.Saved.Remove(value); } - /// Raised after the player loads a save slot and the world is initialised. + /// Raised after the player loads a save slot and the world is initialized. public event EventHandler SaveLoaded { add => this.EventManager.SaveLoaded.Add(value); diff --git a/src/SMAPI/Framework/Events/ModSpecialisedEvents.cs b/src/SMAPI/Framework/Events/ModSpecialisedEvents.cs index 7c3e9dee..9388bdb2 100644 --- a/src/SMAPI/Framework/Events/ModSpecialisedEvents.cs +++ b/src/SMAPI/Framework/Events/ModSpecialisedEvents.cs @@ -3,8 +3,8 @@ using StardewModdingAPI.Events; namespace StardewModdingAPI.Framework.Events { - /// Events serving specialised edge cases that shouldn't be used by most mods. - internal class ModSpecialisedEvents : ModEventsBase, ISpecialisedEvents + /// Events serving specialized edge cases that shouldn't be used by most mods. + internal class ModSpecializedEvents : ModEventsBase, ISpecializedEvents { /********* ** Accessors @@ -37,7 +37,7 @@ namespace StardewModdingAPI.Framework.Events /// Construct an instance. /// The mod which uses this instance. /// The underlying event manager. - internal ModSpecialisedEvents(IModMetadata mod, EventManager eventManager) + internal ModSpecializedEvents(IModMetadata mod, EventManager eventManager) : base(mod, eventManager) { } } } diff --git a/src/SMAPI/Framework/GameVersion.cs b/src/SMAPI/Framework/GameVersion.cs index 261de374..b9ef12c8 100644 --- a/src/SMAPI/Framework/GameVersion.cs +++ b/src/SMAPI/Framework/GameVersion.cs @@ -18,7 +18,7 @@ namespace StardewModdingAPI.Framework ["1.04"] = "1.0.4", ["1.05"] = "1.0.5", ["1.051"] = "1.0.6-prerelease1", // not a very good mapping, but good enough for SMAPI's purposes. - ["1.051b"] = "1.0.6-prelease2", + ["1.051b"] = "1.0.6-prerelease2", ["1.06"] = "1.0.6", ["1.07"] = "1.0.7", ["1.07a"] = "1.0.8-prerelease1", diff --git a/src/SMAPI/Framework/InternalExtensions.cs b/src/SMAPI/Framework/InternalExtensions.cs index f52bfe2b..c3155b1c 100644 --- a/src/SMAPI/Framework/InternalExtensions.cs +++ b/src/SMAPI/Framework/InternalExtensions.cs @@ -55,7 +55,7 @@ namespace StardewModdingAPI.Framework ** Exceptions ****/ /// Get a string representation of an exception suitable for writing to the error log. - /// The error to summarise. + /// The error to summarize. public static string GetLogSummary(this Exception exception) { switch (exception) diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs index 15b164b1..043ae376 100644 --- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs @@ -87,7 +87,7 @@ namespace StardewModdingAPI.Framework.ModHelpers { try { - this.AssertAndNormaliseAssetName(key); + this.AssertAndNormalizeAssetName(key); switch (source) { case ContentSource.GameContent: @@ -106,12 +106,12 @@ namespace StardewModdingAPI.Framework.ModHelpers } } - /// Normalise an asset name so it's consistent with those generated by the game. This is mainly useful for string comparisons like on generated asset names, and isn't necessary when passing asset names into other content helper methods. + /// Normalize an asset name so it's consistent with those generated by the game. This is mainly useful for string comparisons like on generated asset names, and isn't necessary when passing asset names into other content helper methods. /// The asset key. [Pure] - public string NormaliseAssetName(string assetName) + public string NormalizeAssetName(string assetName) { - return this.ModContentManager.AssertAndNormaliseAssetName(assetName); + return this.ModContentManager.AssertAndNormalizeAssetName(assetName); } /// Get the underlying key in the game's content cache for an asset. This can be used to load custom map tilesheets, but should be avoided when you can use the content API instead. This does not validate whether the asset exists. @@ -123,7 +123,7 @@ namespace StardewModdingAPI.Framework.ModHelpers switch (source) { case ContentSource.GameContent: - return this.GameContentManager.AssertAndNormaliseAssetName(key); + return this.GameContentManager.AssertAndNormalizeAssetName(key); case ContentSource.ModFolder: return this.ModContentManager.GetInternalAssetKey(key); @@ -170,9 +170,9 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The asset key to check. /// The asset key is empty or contains invalid characters. [SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local", Justification = "Parameter is only used for assertion checks by design.")] - private void AssertAndNormaliseAssetName(string key) + private void AssertAndNormalizeAssetName(string key) { - this.ModContentManager.AssertAndNormaliseAssetName(key); + this.ModContentManager.AssertAndNormalizeAssetName(key); if (Path.IsPathRooted(key)) throw new ArgumentException("The asset key must not be an absolute path."); } diff --git a/src/SMAPI/Framework/ModHelpers/ContentPackHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentPackHelper.cs index 34f24d65..acdd82a0 100644 --- a/src/SMAPI/Framework/ModHelpers/ContentPackHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ContentPackHelper.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.IO; -using StardewModdingAPI.Toolkit.Serialisation.Models; +using StardewModdingAPI.Toolkit.Serialization.Models; namespace StardewModdingAPI.Framework.ModHelpers { @@ -38,7 +38,7 @@ namespace StardewModdingAPI.Framework.ModHelpers return this.ContentPacks.Value; } - /// Create a temporary content pack to read files from a directory, using randomised manifest fields. This will generate fake manifest data; any manifest.json in the directory will be ignored. Temporary content packs will not appear in the SMAPI log and update checks will not be performed. + /// Create a temporary content pack to read files from a directory, using randomized manifest fields. This will generate fake manifest data; any manifest.json in the directory will be ignored. Temporary content packs will not appear in the SMAPI log and update checks will not be performed. /// The absolute directory path containing the content pack files. public IContentPack CreateFake(string directoryPath) { diff --git a/src/SMAPI/Framework/ModHelpers/DataHelper.cs b/src/SMAPI/Framework/ModHelpers/DataHelper.cs index 3b5c1752..cc08c42b 100644 --- a/src/SMAPI/Framework/ModHelpers/DataHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/DataHelper.cs @@ -1,7 +1,7 @@ using System; using System.IO; using Newtonsoft.Json; -using StardewModdingAPI.Toolkit.Serialisation; +using StardewModdingAPI.Toolkit.Serialization; using StardewModdingAPI.Toolkit.Utilities; using StardewValley; @@ -40,14 +40,14 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Read data from a JSON file in the mod's folder. /// The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types. /// The file path relative to the mod folder. - /// Returns the deserialised model, or null if the file doesn't exist or is empty. + /// Returns the deserialized model, or null if the file doesn't exist or is empty. /// The is not relative or contains directory climbing (../). public TModel ReadJsonFile(string path) where TModel : class { if (!PathUtilities.IsSafeRelativePath(path)) throw new InvalidOperationException($"You must call {nameof(IModHelper.Data)}.{nameof(this.ReadJsonFile)} with a relative path."); - path = Path.Combine(this.ModFolderPath, PathUtilities.NormalisePathSeparators(path)); + path = Path.Combine(this.ModFolderPath, PathUtilities.NormalizePathSeparators(path)); return this.JsonHelper.ReadJsonFileIfExists(path, out TModel data) ? data : null; @@ -63,7 +63,7 @@ namespace StardewModdingAPI.Framework.ModHelpers if (!PathUtilities.IsSafeRelativePath(path)) throw new InvalidOperationException($"You must call {nameof(IMod.Helper)}.{nameof(IModHelper.Data)}.{nameof(this.WriteJsonFile)} with a relative path (without directory climbing)."); - path = Path.Combine(this.ModFolderPath, PathUtilities.NormalisePathSeparators(path)); + path = Path.Combine(this.ModFolderPath, PathUtilities.NormalizePathSeparators(path)); this.JsonHelper.WriteJsonFile(path, data); } @@ -83,7 +83,7 @@ namespace StardewModdingAPI.Framework.ModHelpers throw new InvalidOperationException($"Can't use {nameof(IMod.Helper)}.{nameof(IModHelper.Data)}.{nameof(this.ReadSaveData)} because this isn't the main player. (Save files are stored on the main player's computer.)"); return Game1.CustomData.TryGetValue(this.GetSaveFileKey(key), out string value) - ? this.JsonHelper.Deserialise(value) + ? this.JsonHelper.Deserialize(value) : null; } @@ -101,7 +101,7 @@ namespace StardewModdingAPI.Framework.ModHelpers string internalKey = this.GetSaveFileKey(key); if (data != null) - Game1.CustomData[internalKey] = this.JsonHelper.Serialise(data, Formatting.None); + Game1.CustomData[internalKey] = this.JsonHelper.Serialize(data, Formatting.None); else Game1.CustomData.Remove(internalKey); } diff --git a/src/SMAPI/Framework/ModHelpers/ModHelper.cs b/src/SMAPI/Framework/ModHelpers/ModHelper.cs index 86e8eb28..25401e23 100644 --- a/src/SMAPI/Framework/ModHelpers/ModHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModHelper.cs @@ -2,7 +2,7 @@ using System; using System.IO; using StardewModdingAPI.Events; using StardewModdingAPI.Framework.Input; -using StardewModdingAPI.Toolkit.Serialisation; +using StardewModdingAPI.Toolkit.Serialization; namespace StardewModdingAPI.Framework.ModHelpers { @@ -73,7 +73,7 @@ namespace StardewModdingAPI.Framework.ModHelpers if (!Directory.Exists(modDirectory)) throw new InvalidOperationException("The specified mod directory does not exist."); - // initialise + // initialize this.DirectoryPath = modDirectory; this.Content = contentHelper ?? throw new ArgumentNullException(nameof(contentHelper)); this.ContentPacks = contentPackHelper ?? throw new ArgumentNullException(nameof(contentPackHelper)); diff --git a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs index 8330e078..24bed3bb 100644 --- a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs @@ -75,9 +75,9 @@ namespace StardewModdingAPI.Framework.ModHelpers public TInterface GetApi(string uniqueID) where TInterface : class { // validate - if (!this.Registry.AreAllModsInitialised) + if (!this.Registry.AreAllModsInitialized) { - this.Monitor.Log("Tried to access a mod-provided API before all mods were initialised.", LogLevel.Error); + this.Monitor.Log("Tried to access a mod-provided API before all mods were initialized.", LogLevel.Error); return null; } if (!typeof(TInterface).IsInterface) diff --git a/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs b/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs index 0ce72a9e..86c327ed 100644 --- a/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs @@ -5,7 +5,7 @@ using StardewModdingAPI.Framework.Reflection; namespace StardewModdingAPI.Framework.ModHelpers { /// Provides helper methods for accessing private game code. - /// This implementation searches up the type hierarchy, and caches the reflected fields and methods with a sliding expiry (to optimise performance without unnecessary memory usage). + /// This implementation searches up the type hierarchy, and caches the reflected fields and methods with a sliding expiry (to optimize performance without unnecessary memory usage). internal class ReflectionHelper : BaseHelper, IReflectionHelper { /********* diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index 8dfacc33..7670eb3a 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -317,7 +317,7 @@ namespace StardewModdingAPI.Framework.ModLoading } /// Process the result from an instruction handler. - /// The mod being analysed. + /// The mod being analyzed. /// The instruction handler. /// The result returned by the handler. /// The messages already logged for the current mod. @@ -341,9 +341,9 @@ namespace StardewModdingAPI.Framework.ModLoading mod.SetWarning(ModWarning.PatchesGame); break; - case InstructionHandleResult.DetectedSaveSerialiser: - this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected possible save serialiser change ({handler.NounPhrase}) in assembly {filename}."); - mod.SetWarning(ModWarning.ChangesSaveSerialiser); + case InstructionHandleResult.DetectedSaveSerializer: + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected possible save serializer change ({handler.NounPhrase}) in assembly {filename}."); + mod.SetWarning(ModWarning.ChangesSaveSerializer); break; case InstructionHandleResult.DetectedUnvalidatedUpdateTick: @@ -370,7 +370,7 @@ namespace StardewModdingAPI.Framework.ModLoading break; default: - throw new NotSupportedException($"Unrecognised instruction handler result '{result}'."); + throw new NotSupportedException($"Unrecognized instruction handler result '{result}'."); } } diff --git a/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs index 79045241..701b15f2 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs @@ -73,7 +73,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders ** Protected methods *********/ /// Get whether a CIL instruction matches. - /// The method deifnition. + /// The method definition. protected bool IsMatch(MethodDefinition method) { if (this.IsMatch(method.ReturnType)) diff --git a/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs b/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs index 6592760e..d93b603d 100644 --- a/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs +++ b/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs @@ -18,12 +18,12 @@ namespace StardewModdingAPI.Framework.ModLoading DetectedGamePatch, /// The instruction is compatible, but affects the save serializer in a way that may make saves unloadable without the mod. - DetectedSaveSerialiser, + DetectedSaveSerializer, /// The instruction is compatible, but uses the dynamic keyword which won't work on Linux/Mac. DetectedDynamic, - /// The instruction is compatible, but references or which may impact stability. + /// The instruction is compatible, but references or which may impact stability. DetectedUnvalidatedUpdateTick, /// The instruction accesses the filesystem directly. diff --git a/src/SMAPI/Framework/ModLoading/ModDependencyStatus.cs b/src/SMAPI/Framework/ModLoading/ModDependencyStatus.cs index 0774b487..dd855d2f 100644 --- a/src/SMAPI/Framework/ModLoading/ModDependencyStatus.cs +++ b/src/SMAPI/Framework/ModLoading/ModDependencyStatus.cs @@ -1,4 +1,4 @@ -namespace StardewModdingAPI.Framework.ModLoading +namespace StardewModdingAPI.Framework.ModLoading { /// The status of a given mod in the dependency-sorting algorithm. internal enum ModDependencyStatus @@ -6,7 +6,7 @@ /// The mod hasn't been visited yet. Queued, - /// The mod is currently being analysed as part of a dependency chain. + /// The mod is currently being analyzed as part of a dependency chain. Checking, /// The mod has already been sorted. diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 20941a5f..5ea21710 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -5,7 +5,7 @@ using System.Linq; using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.ModData; using StardewModdingAPI.Toolkit.Framework.ModScanning; -using StardewModdingAPI.Toolkit.Serialisation.Models; +using StardewModdingAPI.Toolkit.Serialization.Models; using StardewModdingAPI.Toolkit.Utilities; namespace StardewModdingAPI.Framework.ModLoading @@ -143,11 +143,11 @@ namespace StardewModdingAPI.Framework.ModLoading continue; } - // invalid capitalisation + // invalid capitalization string actualFilename = new DirectoryInfo(mod.DirectoryPath).GetFiles(mod.Manifest.EntryDll).FirstOrDefault()?.Name; if (actualFilename != mod.Manifest.EntryDll) { - mod.SetStatus(ModMetadataStatus.Failed, $"its {nameof(IManifest.EntryDll)} value '{mod.Manifest.EntryDll}' doesn't match the actual file capitalisation '{actualFilename}'. The capitalisation must match for crossplatform compatibility."); + mod.SetStatus(ModMetadataStatus.Failed, $"its {nameof(IManifest.EntryDll)} value '{mod.Manifest.EntryDll}' doesn't match the actual file capitalization '{actualFilename}'. The capitalization must match for crossplatform compatibility."); continue; } } @@ -216,7 +216,7 @@ namespace StardewModdingAPI.Framework.ModLoading /// Handles access to SMAPI's internal mod metadata list. public IEnumerable ProcessDependencies(IEnumerable mods, ModDatabase modDatabase) { - // initialise metadata + // initialize metadata mods = mods.ToArray(); var sortedMods = new Stack(); var states = mods.ToDictionary(mod => mod, mod => ModDependencyStatus.Queued); diff --git a/src/SMAPI/Framework/ModLoading/TypeReferenceComparer.cs b/src/SMAPI/Framework/ModLoading/TypeReferenceComparer.cs index f7497789..a4ac54e2 100644 --- a/src/SMAPI/Framework/ModLoading/TypeReferenceComparer.cs +++ b/src/SMAPI/Framework/ModLoading/TypeReferenceComparer.cs @@ -54,7 +54,7 @@ namespace StardewModdingAPI.Framework.ModLoading { bool HeuristicallyEquals(string typeNameA, string typeNameB, IDictionary tokenMap) { - // analyse type names + // analyze type names bool hasTokensA = typeNameA.Contains("!"); bool hasTokensB = typeNameB.Contains("!"); bool isTokenA = hasTokensA && typeNameA[0] == '!'; diff --git a/src/SMAPI/Framework/ModRegistry.cs b/src/SMAPI/Framework/ModRegistry.cs index 5be33cb4..ef389337 100644 --- a/src/SMAPI/Framework/ModRegistry.cs +++ b/src/SMAPI/Framework/ModRegistry.cs @@ -21,8 +21,8 @@ namespace StardewModdingAPI.Framework /// Whether all mod assemblies have been loaded. public bool AreAllModsLoaded { get; set; } - /// Whether all mods have been initialised and their method called. - public bool AreAllModsInitialised { get; set; } + /// Whether all mods have been initialized and their method called. + public bool AreAllModsInitialized { get; set; } /********* @@ -62,7 +62,7 @@ namespace StardewModdingAPI.Framework /// Returns the matching mod's metadata, or null if not found. public IModMetadata Get(string uniqueID) { - // normalise search ID + // normalize search ID if (string.IsNullOrWhiteSpace(uniqueID)) return null; uniqueID = uniqueID.Trim(); diff --git a/src/SMAPI/Framework/Monitor.cs b/src/SMAPI/Framework/Monitor.cs index 3771f114..1fa55a9e 100644 --- a/src/SMAPI/Framework/Monitor.cs +++ b/src/SMAPI/Framework/Monitor.cs @@ -58,7 +58,7 @@ namespace StardewModdingAPI.Framework if (string.IsNullOrWhiteSpace(source)) throw new ArgumentException("The log source cannot be empty."); - // initialise + // initialize this.Source = source; this.LogFile = logFile ?? throw new ArgumentNullException(nameof(logFile), "The log file manager cannot be null."); this.ConsoleWriter = new ColorfulConsoleWriter(Constants.Platform, colorScheme); diff --git a/src/SMAPI/Framework/Networking/MessageType.cs b/src/SMAPI/Framework/Networking/MessageType.cs index bd9acfa9..4e1388ca 100644 --- a/src/SMAPI/Framework/Networking/MessageType.cs +++ b/src/SMAPI/Framework/Networking/MessageType.cs @@ -2,7 +2,7 @@ using StardewValley; namespace StardewModdingAPI.Framework.Networking { - /// Network message types recognised by SMAPI and Stardew Valley. + /// Network message types recognized by SMAPI and Stardew Valley. internal enum MessageType : byte { /********* diff --git a/src/SMAPI/Framework/Reflection/Reflector.cs b/src/SMAPI/Framework/Reflection/Reflector.cs index ed1a4381..d4904878 100644 --- a/src/SMAPI/Framework/Reflection/Reflector.cs +++ b/src/SMAPI/Framework/Reflection/Reflector.cs @@ -6,7 +6,7 @@ using System.Runtime.Caching; namespace StardewModdingAPI.Framework.Reflection { /// Provides helper methods for accessing inaccessible code. - /// This implementation searches up the type hierarchy, and caches the reflected fields and methods with a sliding expiry (to optimise performance without unnecessary memory usage). + /// This implementation searches up the type hierarchy, and caches the reflected fields and methods with a sliding expiry (to optimize performance without unnecessary memory usage). internal class Reflector { /********* diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 0aae3b84..c5dede01 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -24,13 +24,13 @@ using StardewModdingAPI.Framework.ModHelpers; using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Framework.Patching; using StardewModdingAPI.Framework.Reflection; -using StardewModdingAPI.Framework.Serialisation; +using StardewModdingAPI.Framework.Serialization; using StardewModdingAPI.Internal; using StardewModdingAPI.Patches; using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; using StardewModdingAPI.Toolkit.Framework.ModData; -using StardewModdingAPI.Toolkit.Serialisation; +using StardewModdingAPI.Toolkit.Serialization; using StardewModdingAPI.Toolkit.Utilities; using StardewValley; using Object = StardewValley.Object; @@ -38,7 +38,7 @@ using ThreadState = System.Threading.ThreadState; namespace StardewModdingAPI.Framework { - /// The core class which initialises and manages SMAPI. + /// The core class which initializes and manages SMAPI. internal class SCore : IDisposable { /********* @@ -56,7 +56,7 @@ namespace StardewModdingAPI.Framework /// The core logger and monitor on behalf of the game. private readonly Monitor MonitorForGame; - /// Tracks whether the game should exit immediately and any pending initialisation should be cancelled. + /// Tracks whether the game should exit immediately and any pending initialization should be cancelled. private readonly CancellationTokenSource CancellationToken = new CancellationTokenSource(); /// Simplifies access to private game code. @@ -72,7 +72,7 @@ namespace StardewModdingAPI.Framework private ContentCoordinator ContentCore => this.GameInstance.ContentCore; /// Tracks the installed mods. - /// This is initialised after the game starts. + /// This is initialized after the game starts. private readonly ModRegistry ModRegistry = new ModRegistry(); /// Manages SMAPI events for mods. @@ -120,7 +120,7 @@ namespace StardewModdingAPI.Framework ** Accessors *********/ /// Manages deprecation warnings. - /// This is initialised after the game starts. This is accessed directly because it's not part of the normal class model. + /// This is initialized after the game starts. This is accessed directly because it's not part of the normal class model. internal static DeprecationManager DeprecationManager { get; private set; } @@ -187,7 +187,7 @@ namespace StardewModdingAPI.Framework [HandleProcessCorruptedStateExceptions, SecurityCritical] // let try..catch handle corrupted state exceptions public void RunInteractively() { - // initialise SMAPI + // initialize SMAPI try { JsonConverter[] converters = { @@ -205,14 +205,14 @@ namespace StardewModdingAPI.Framework #endif AppDomain.CurrentDomain.UnhandledException += (sender, e) => this.Monitor.Log($"Critical app domain exception: {e.ExceptionObject}", LogLevel.Error); - // add more leniant assembly resolvers + // add more lenient assembly resolvers AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => AssemblyLoader.ResolveAssembly(e.Name); // hook locale event LocalizedContentManager.OnLanguageChange += locale => this.OnLocaleChanged(); // override game - SGame.ConstructorHack = new SGameConstructorHack(this.Monitor, this.Reflection, this.Toolkit.JsonHelper, this.InitialiseBeforeFirstAssetLoaded); + SGame.ConstructorHack = new SGameConstructorHack(this.Monitor, this.Reflection, this.Toolkit.JsonHelper, this.InitializeBeforeFirstAssetLoaded); this.GameInstance = new SGame( monitor: this.Monitor, monitorForGame: this.MonitorForGame, @@ -221,7 +221,7 @@ namespace StardewModdingAPI.Framework jsonHelper: this.Toolkit.JsonHelper, modRegistry: this.ModRegistry, deprecationManager: SCore.DeprecationManager, - onGameInitialised: this.InitialiseAfterGameStart, + onGameInitialized: this.InitializeAfterGameStart, onGameExiting: this.Dispose, cancellationToken: this.CancellationToken, logNetworkTraffic: this.Settings.LogNetworkTraffic @@ -262,7 +262,7 @@ namespace StardewModdingAPI.Framework } catch (Exception ex) { - this.Monitor.Log($"SMAPI failed to initialise: {ex.GetLogSummary()}", LogLevel.Error); + this.Monitor.Log($"SMAPI failed to initialize: {ex.GetLogSummary()}", LogLevel.Error); this.PressAnyKeyToExit(); return; } @@ -377,12 +377,12 @@ namespace StardewModdingAPI.Framework /********* ** Private methods *********/ - /// Initialise mods before the first game asset is loaded. At this point the core content managers are loaded (so mods can load their own assets), but the game is mostly uninitialised. - private void InitialiseBeforeFirstAssetLoaded() + /// Initialize mods before the first game asset is loaded. At this point the core content managers are loaded (so mods can load their own assets), but the game is mostly uninitialized. + private void InitializeBeforeFirstAssetLoaded() { if (this.CancellationToken.IsCancellationRequested) { - this.Monitor.Log("SMAPI shutting down: aborting initialisation.", LogLevel.Warn); + this.Monitor.Log("SMAPI shutting down: aborting initialization.", LogLevel.Warn); return; } @@ -432,8 +432,8 @@ namespace StardewModdingAPI.Framework Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion} with {modsLoaded} mods"; } - /// Initialise SMAPI and mods after the game starts. - private void InitialiseAfterGameStart() + /// Initialize SMAPI and mods after the game starts. + private void InitializeAfterGameStart() { // validate XNB integrity if (!this.ValidateContentIntegrity()) @@ -696,7 +696,7 @@ namespace StardewModdingAPI.Framework /// Get whether a given version should be offered to the user as an update. /// The current semantic version. /// The target semantic version. - /// Whether the user enabled the beta channel and should be offered pre-release updates. + /// Whether the user enabled the beta channel and should be offered prerelease updates. private bool IsValidUpdate(ISemanticVersion currentVersion, ISemanticVersion newVersion, bool useBetaChannel) { return @@ -716,7 +716,7 @@ namespace StardewModdingAPI.Framework } catch (Exception ex) { - // note: this happens before this.Monitor is initialised + // note: this happens before this.Monitor is initialized Console.WriteLine($"Couldn't create a path: {path}\n\n{ex.GetLogSummary()}"); } } @@ -795,10 +795,10 @@ namespace StardewModdingAPI.Framework // log mod warnings this.LogModWarnings(loaded, skippedMods); - // initialise translations + // initialize translations this.ReloadTranslations(loaded); - // initialise loaded non-content-pack mods + // initialize loaded non-content-pack mods foreach (IModMetadata metadata in loadedMods) { // add interceptors @@ -847,7 +847,7 @@ namespace StardewModdingAPI.Framework } // invalidate cache entries when needed - // (These listeners are registered after Entry to avoid repeatedly reloading assets as mods initialise.) + // (These listeners are registered after Entry to avoid repeatedly reloading assets as mods initialize.) foreach (IModMetadata metadata in loadedMods) { if (metadata.Mod.Helper.Content is ContentHelper helper) @@ -881,7 +881,7 @@ namespace StardewModdingAPI.Framework } // unlock mod integrations - this.ModRegistry.AreAllModsInitialised = true; + this.ModRegistry.AreAllModsInitialized = true; } /// Load a given mod. @@ -924,7 +924,7 @@ namespace StardewModdingAPI.Framework } // validate dependencies - // Although dependences are validated before mods are loaded, a dependency may have failed to load. + // Although dependencies are validated before mods are loaded, a dependency may have failed to load. if (mod.Manifest.Dependencies?.Any() == true) { foreach (IManifestDependency dependency in mod.Manifest.Dependencies.Where(p => p.IsRequired)) @@ -988,7 +988,7 @@ namespace StardewModdingAPI.Framework return false; } - // initialise mod + // initialize mod try { // get mod instance @@ -1045,7 +1045,7 @@ namespace StardewModdingAPI.Framework } catch (Exception ex) { - errorReasonPhrase = $"initialisation failed:\n{ex.GetLogSummary()}"; + errorReasonPhrase = $"initialization failed:\n{ex.GetLogSummary()}"; return false; } } @@ -1120,8 +1120,8 @@ namespace StardewModdingAPI.Framework "These mods have broken code, but you configured SMAPI to load them anyway. This may cause bugs,", "errors, or crashes in-game." ); - LogWarningGroup(ModWarning.ChangesSaveSerialiser, LogLevel.Warn, "Changed save serialiser", - "These mods change the save serialiser. They may corrupt your save files, or make them unusable if", + LogWarningGroup(ModWarning.ChangesSaveSerializer, LogLevel.Warn, "Changed save serializer", + "These mods change the save serializer. They may corrupt your save files, or make them unusable if", "you uninstall these mods." ); if (this.Settings.ParanoidWarnings) @@ -1285,7 +1285,7 @@ namespace StardewModdingAPI.Framework break; default: - throw new NotSupportedException($"Unrecognise core SMAPI command '{name}'."); + throw new NotSupportedException($"Unrecognized core SMAPI command '{name}'."); } } diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 376368d6..e6d91fc3 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -18,7 +18,7 @@ using StardewModdingAPI.Framework.Networking; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.StateTracking.Snapshots; using StardewModdingAPI.Framework.Utilities; -using StardewModdingAPI.Toolkit.Serialisation; +using StardewModdingAPI.Toolkit.Serialization; using StardewValley; using StardewValley.BellsAndWhistles; using StardewValley.Events; @@ -60,7 +60,7 @@ namespace StardewModdingAPI.Framework private readonly Countdown UpdateCrashTimer = new Countdown(60); // 60 ticks = roughly one second /// The number of ticks until SMAPI should notify mods that the game has loaded. - /// Skipping a few frames ensures the game finishes initialising the world before mods try to change it. + /// Skipping a few frames ensures the game finishes initializing the world before mods try to change it. private readonly Countdown AfterLoadTimer = new Countdown(5); /// Whether the game is saving and SMAPI has already raised . @@ -72,8 +72,8 @@ namespace StardewModdingAPI.Framework /// A callback to invoke the first time *any* game content manager loads an asset. private readonly Action OnLoadingFirstAsset; - /// A callback to invoke after the game finishes initialising. - private readonly Action OnGameInitialised; + /// A callback to invoke after the game finishes initializing. + private readonly Action OnGameInitialized; /// A callback to invoke when the game exits. private readonly Action OnGameExiting; @@ -93,8 +93,8 @@ namespace StardewModdingAPI.Framework /// A snapshot of the current state. private WatcherSnapshot WatcherSnapshot = new WatcherSnapshot(); - /// Whether post-game-startup initialisation has been performed. - private bool IsInitialised; + /// Whether post-game-startup initialization has been performed. + private bool IsInitialized; /// Whether the next content manager requested by the game will be for . private bool NextContentManagerIsMain; @@ -103,7 +103,7 @@ namespace StardewModdingAPI.Framework /********* ** Accessors *********/ - /// Static state to use while is initialising, which happens before the constructor runs. + /// Static state to use while is initializing, which happens before the constructor runs. internal static SGameConstructorHack ConstructorHack { get; set; } /// The number of update ticks which have already executed. This is similar to , but incremented more consistently for every tick. @@ -137,18 +137,18 @@ namespace StardewModdingAPI.Framework /// Encapsulates SMAPI's JSON file parsing. /// Tracks the installed mods. /// Manages deprecation warnings. - /// A callback to invoke after the game finishes initialising. + /// A callback to invoke after the game finishes initializing. /// A callback to invoke when the game exits. /// Propagates notification that SMAPI should exit. /// Whether to log network traffic. - internal SGame(Monitor monitor, IMonitor monitorForGame, Reflector reflection, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, DeprecationManager deprecationManager, Action onGameInitialised, Action onGameExiting, CancellationTokenSource cancellationToken, bool logNetworkTraffic) + internal SGame(Monitor monitor, IMonitor monitorForGame, Reflector reflection, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, DeprecationManager deprecationManager, Action onGameInitialized, Action onGameExiting, CancellationTokenSource cancellationToken, bool logNetworkTraffic) { this.OnLoadingFirstAsset = SGame.ConstructorHack.OnLoadingFirstAsset; SGame.ConstructorHack = null; // check expectations if (this.ContentCore == null) - throw new InvalidOperationException($"The game didn't initialise its first content manager before SMAPI's {nameof(SGame)} constructor. This indicates an incompatible lifecycle change."); + throw new InvalidOperationException($"The game didn't initialize its first content manager before SMAPI's {nameof(SGame)} constructor. This indicates an incompatible lifecycle change."); // init XNA Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef; @@ -160,7 +160,7 @@ namespace StardewModdingAPI.Framework this.ModRegistry = modRegistry; this.Reflection = reflection; this.DeprecationManager = deprecationManager; - this.OnGameInitialised = onGameInitialised; + this.OnGameInitialized = onGameInitialized; this.OnGameExiting = onGameExiting; Game1.input = new SInputState(); Game1.multiplayer = new SMultiplayer(monitor, eventManager, jsonHelper, modRegistry, reflection, this.OnModMessageReceived, logNetworkTraffic); @@ -171,8 +171,8 @@ namespace StardewModdingAPI.Framework Game1.locations = new ObservableCollection(); } - /// Initialise just before the game's first update tick. - private void InitialiseAfterGameStarted() + /// Initialize just before the game's first update tick. + private void InitializeAfterGameStarted() { // set initial state this.Input.TrueUpdate(); @@ -181,7 +181,7 @@ namespace StardewModdingAPI.Framework this.Watchers = new WatcherCore(this.Input); // raise callback - this.OnGameInitialised(); + this.OnGameInitialized(); } /// Perform cleanup logic when the game exits. @@ -238,8 +238,8 @@ namespace StardewModdingAPI.Framework /// The root directory to search for content. protected override LocalizedContentManager CreateContentManager(IServiceProvider serviceProvider, string rootDirectory) { - // Game1._temporaryContent initialising from SGame constructor - // NOTE: this method is called before the SGame constructor runs. Don't depend on anything being initialised at this point. + // Game1._temporaryContent initializing from SGame constructor + // NOTE: this method is called before the SGame constructor runs. Don't depend on anything being initialized at this point. if (this.ContentCore == null) { this.ContentCore = new ContentCoordinator(serviceProvider, rootDirectory, Thread.CurrentThread.CurrentUICulture, SGame.ConstructorHack.Monitor, SGame.ConstructorHack.Reflection, SGame.ConstructorHack.JsonHelper, this.OnLoadingFirstAsset ?? SGame.ConstructorHack?.OnLoadingFirstAsset); @@ -247,7 +247,7 @@ namespace StardewModdingAPI.Framework return this.ContentCore.CreateGameContentManager("Game1._temporaryContent"); } - // Game1.content initialising from LoadContent + // Game1.content initializing from LoadContent if (this.NextContentManagerIsMain) { this.NextContentManagerIsMain = false; @@ -269,12 +269,12 @@ namespace StardewModdingAPI.Framework this.DeprecationManager.PrintQueued(); /********* - ** First-tick initialisation + ** First-tick initialization *********/ - if (!this.IsInitialised) + if (!this.IsInitialized) { - this.IsInitialised = true; - this.InitialiseAfterGameStarted(); + this.IsInitialized = true; + this.InitializeAfterGameStarted(); } /********* @@ -302,7 +302,7 @@ namespace StardewModdingAPI.Framework bool saveParsed = false; if (Game1.currentLoader != null) { - this.Monitor.Log("Game loader synchronising...", LogLevel.Trace); + this.Monitor.Log("Game loader synchronizing...", LogLevel.Trace); while (Game1.currentLoader?.MoveNext() == true) { // raise load stage changed @@ -333,7 +333,7 @@ namespace StardewModdingAPI.Framework } if (Game1._newDayTask?.Status == TaskStatus.Created) { - this.Monitor.Log("New day task synchronising...", LogLevel.Trace); + this.Monitor.Log("New day task synchronizing...", LogLevel.Trace); Game1._newDayTask.RunSynchronously(); this.Monitor.Log("New day task done.", LogLevel.Trace); } @@ -346,7 +346,7 @@ namespace StardewModdingAPI.Framework // Therefore we can just run Game1.Update here without raising any SMAPI events. There's // a small chance that the task will finish after we defer but before the game checks, // which means technically events should be raised, but the effects of missing one - // update tick are neglible and not worth the complications of bypassing Game1.Update. + // update tick are negligible and not worth the complications of bypassing Game1.Update. if (Game1._newDayTask != null || Game1.gameMode == Game1.loadingMode) { events.UnvalidatedUpdateTicking.RaiseEmpty(); @@ -436,7 +436,7 @@ namespace StardewModdingAPI.Framework } else if (Context.IsSaveLoaded && this.AfterLoadTimer.Current > 0 && Game1.currentLocation != null) { - if (Game1.dayOfMonth != 0) // wait until new-game intro finishes (world not fully initialised yet) + if (Game1.dayOfMonth != 0) // wait until new-game intro finishes (world not fully initialized yet) this.AfterLoadTimer.Decrement(); Context.IsWorldReady = this.AfterLoadTimer.Current == 0; } diff --git a/src/SMAPI/Framework/SGameConstructorHack.cs b/src/SMAPI/Framework/SGameConstructorHack.cs index c3d22197..f70dec03 100644 --- a/src/SMAPI/Framework/SGameConstructorHack.cs +++ b/src/SMAPI/Framework/SGameConstructorHack.cs @@ -1,11 +1,11 @@ using System; using StardewModdingAPI.Framework.Reflection; -using StardewModdingAPI.Toolkit.Serialisation; +using StardewModdingAPI.Toolkit.Serialization; using StardewValley; namespace StardewModdingAPI.Framework { - /// The static state to use while is initialising, which happens before the constructor runs. + /// The static state to use while is initializing, which happens before the constructor runs. internal class SGameConstructorHack { /********* diff --git a/src/SMAPI/Framework/SMultiplayer.cs b/src/SMAPI/Framework/SMultiplayer.cs index 531c229e..e04205c8 100644 --- a/src/SMAPI/Framework/SMultiplayer.cs +++ b/src/SMAPI/Framework/SMultiplayer.cs @@ -9,7 +9,7 @@ using StardewModdingAPI.Events; using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Networking; using StardewModdingAPI.Framework.Reflection; -using StardewModdingAPI.Toolkit.Serialisation; +using StardewModdingAPI.Toolkit.Serialization; using StardewValley; using StardewValley.Network; using StardewValley.SDKs; @@ -94,8 +94,8 @@ namespace StardewModdingAPI.Framework this.HostPeer = null; } - /// Initialise a client before the game connects to a remote server. - /// The client to initialise. + /// Initialize a client before the game connects to a remote server. + /// The client to initialize. public override Client InitClient(Client client) { switch (client) @@ -118,8 +118,8 @@ namespace StardewModdingAPI.Framework } } - /// Initialise a server before the game connects to an incoming player. - /// The server to initialise. + /// Initialize a server before the game connects to an incoming player. + /// The server to initialize. public override Server InitServer(Server server) { switch (server) @@ -423,7 +423,7 @@ namespace StardewModdingAPI.Framework private RemoteContextModel ReadContext(BinaryReader reader) { string data = reader.ReadString(); - RemoteContextModel model = this.JsonHelper.Deserialise(data); + RemoteContextModel model = this.JsonHelper.Deserialize(data); return model.ApiVersion != null ? model : null; // no data available for unmodded players @@ -435,7 +435,7 @@ namespace StardewModdingAPI.Framework { // parse message string json = message.Reader.ReadString(); - ModMessageModel model = this.JsonHelper.Deserialise(json); + ModMessageModel model = this.JsonHelper.Deserialize(json); HashSet playerIDs = new HashSet(model.ToPlayerIDs ?? this.GetKnownPlayerIDs()); if (this.LogNetworkTraffic) this.Monitor.Log($"Received message: {json}.", LogLevel.Trace); @@ -454,7 +454,7 @@ namespace StardewModdingAPI.Framework { newModel.ToPlayerIDs = new[] { peer.PlayerID }; this.Monitor.VerboseLog($" Forwarding message to player {peer.PlayerID}."); - peer.SendMessage(new OutgoingMessage((byte)MessageType.ModMessage, peer.PlayerID, this.JsonHelper.Serialise(newModel, Formatting.None))); + peer.SendMessage(new OutgoingMessage((byte)MessageType.ModMessage, peer.PlayerID, this.JsonHelper.Serialize(newModel, Formatting.None))); } } } @@ -488,7 +488,7 @@ namespace StardewModdingAPI.Framework .ToArray() }; - return new object[] { this.JsonHelper.Serialise(model, Formatting.None) }; + return new object[] { this.JsonHelper.Serialize(model, Formatting.None) }; } /// Get the fields to include in a context sync message sent to other players. @@ -514,7 +514,7 @@ namespace StardewModdingAPI.Framework .ToArray() }; - return new object[] { this.JsonHelper.Serialise(model, Formatting.None) }; + return new object[] { this.JsonHelper.Serialize(model, Formatting.None) }; } } } diff --git a/src/SMAPI/Framework/Serialisation/ColorConverter.cs b/src/SMAPI/Framework/Serialisation/ColorConverter.cs deleted file mode 100644 index c27065bf..00000000 --- a/src/SMAPI/Framework/Serialisation/ColorConverter.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using Microsoft.Xna.Framework; -using Newtonsoft.Json.Linq; -using StardewModdingAPI.Toolkit.Serialisation; -using StardewModdingAPI.Toolkit.Serialisation.Converters; - -namespace StardewModdingAPI.Framework.Serialisation -{ - /// Handles deserialisation of for crossplatform compatibility. - /// - /// - Linux/Mac format: { "B": 76, "G": 51, "R": 25, "A": 102 } - /// - Windows format: "26, 51, 76, 102" - /// - internal class ColorConverter : SimpleReadOnlyConverter - { - /********* - ** Protected methods - *********/ - /// Read a JSON object. - /// The JSON object to read. - /// The path to the current JSON node. - protected override Color ReadObject(JObject obj, string path) - { - int r = obj.ValueIgnoreCase(nameof(Color.R)); - int g = obj.ValueIgnoreCase(nameof(Color.G)); - int b = obj.ValueIgnoreCase(nameof(Color.B)); - int a = obj.ValueIgnoreCase(nameof(Color.A)); - return new Color(r, g, b, a); - } - - /// Read a JSON string. - /// The JSON string value. - /// The path to the current JSON node. - protected override Color ReadString(string str, string path) - { - string[] parts = str.Split(','); - if (parts.Length != 4) - throw new SParseException($"Can't parse {typeof(Color).Name} from invalid value '{str}' (path: {path})."); - - int r = Convert.ToInt32(parts[0]); - int g = Convert.ToInt32(parts[1]); - int b = Convert.ToInt32(parts[2]); - int a = Convert.ToInt32(parts[3]); - return new Color(r, g, b, a); - } - } -} diff --git a/src/SMAPI/Framework/Serialisation/PointConverter.cs b/src/SMAPI/Framework/Serialisation/PointConverter.cs deleted file mode 100644 index fbc857d2..00000000 --- a/src/SMAPI/Framework/Serialisation/PointConverter.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using Microsoft.Xna.Framework; -using Newtonsoft.Json.Linq; -using StardewModdingAPI.Toolkit.Serialisation; -using StardewModdingAPI.Toolkit.Serialisation.Converters; - -namespace StardewModdingAPI.Framework.Serialisation -{ - /// Handles deserialisation of for crossplatform compatibility. - /// - /// - Linux/Mac format: { "X": 1, "Y": 2 } - /// - Windows format: "1, 2" - /// - internal class PointConverter : SimpleReadOnlyConverter - { - /********* - ** Protected methods - *********/ - /// Read a JSON object. - /// The JSON object to read. - /// The path to the current JSON node. - protected override Point ReadObject(JObject obj, string path) - { - int x = obj.ValueIgnoreCase(nameof(Point.X)); - int y = obj.ValueIgnoreCase(nameof(Point.Y)); - return new Point(x, y); - } - - /// Read a JSON string. - /// The JSON string value. - /// The path to the current JSON node. - protected override Point ReadString(string str, string path) - { - string[] parts = str.Split(','); - if (parts.Length != 2) - throw new SParseException($"Can't parse {typeof(Point).Name} from invalid value '{str}' (path: {path})."); - - int x = Convert.ToInt32(parts[0]); - int y = Convert.ToInt32(parts[1]); - return new Point(x, y); - } - } -} diff --git a/src/SMAPI/Framework/Serialisation/RectangleConverter.cs b/src/SMAPI/Framework/Serialisation/RectangleConverter.cs deleted file mode 100644 index 4f55cc32..00000000 --- a/src/SMAPI/Framework/Serialisation/RectangleConverter.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.Text.RegularExpressions; -using Microsoft.Xna.Framework; -using Newtonsoft.Json.Linq; -using StardewModdingAPI.Toolkit.Serialisation; -using StardewModdingAPI.Toolkit.Serialisation.Converters; - -namespace StardewModdingAPI.Framework.Serialisation -{ - /// Handles deserialisation of for crossplatform compatibility. - /// - /// - Linux/Mac format: { "X": 1, "Y": 2, "Width": 3, "Height": 4 } - /// - Windows format: "{X:1 Y:2 Width:3 Height:4}" - /// - internal class RectangleConverter : SimpleReadOnlyConverter - { - /********* - ** Protected methods - *********/ - /// Read a JSON object. - /// The JSON object to read. - /// The path to the current JSON node. - protected override Rectangle ReadObject(JObject obj, string path) - { - int x = obj.ValueIgnoreCase(nameof(Rectangle.X)); - int y = obj.ValueIgnoreCase(nameof(Rectangle.Y)); - int width = obj.ValueIgnoreCase(nameof(Rectangle.Width)); - int height = obj.ValueIgnoreCase(nameof(Rectangle.Height)); - return new Rectangle(x, y, width, height); - } - - /// Read a JSON string. - /// The JSON string value. - /// The path to the current JSON node. - protected override Rectangle ReadString(string str, string path) - { - if (string.IsNullOrWhiteSpace(str)) - return Rectangle.Empty; - - var match = Regex.Match(str, @"^\{X:(?\d+) Y:(?\d+) Width:(?\d+) Height:(?\d+)\}$", RegexOptions.IgnoreCase); - if (!match.Success) - throw new SParseException($"Can't parse {typeof(Rectangle).Name} from invalid value '{str}' (path: {path})."); - - int x = Convert.ToInt32(match.Groups["x"].Value); - int y = Convert.ToInt32(match.Groups["y"].Value); - int width = Convert.ToInt32(match.Groups["width"].Value); - int height = Convert.ToInt32(match.Groups["height"].Value); - - return new Rectangle(x, y, width, height); - } - } -} diff --git a/src/SMAPI/Framework/Serialization/ColorConverter.cs b/src/SMAPI/Framework/Serialization/ColorConverter.cs new file mode 100644 index 00000000..19979981 --- /dev/null +++ b/src/SMAPI/Framework/Serialization/ColorConverter.cs @@ -0,0 +1,47 @@ +using System; +using Microsoft.Xna.Framework; +using Newtonsoft.Json.Linq; +using StardewModdingAPI.Toolkit.Serialization; +using StardewModdingAPI.Toolkit.Serialization.Converters; + +namespace StardewModdingAPI.Framework.Serialization +{ + /// Handles deserialization of for crossplatform compatibility. + /// + /// - Linux/Mac format: { "B": 76, "G": 51, "R": 25, "A": 102 } + /// - Windows format: "26, 51, 76, 102" + /// + internal class ColorConverter : SimpleReadOnlyConverter + { + /********* + ** Protected methods + *********/ + /// Read a JSON object. + /// The JSON object to read. + /// The path to the current JSON node. + protected override Color ReadObject(JObject obj, string path) + { + int r = obj.ValueIgnoreCase(nameof(Color.R)); + int g = obj.ValueIgnoreCase(nameof(Color.G)); + int b = obj.ValueIgnoreCase(nameof(Color.B)); + int a = obj.ValueIgnoreCase(nameof(Color.A)); + return new Color(r, g, b, a); + } + + /// Read a JSON string. + /// The JSON string value. + /// The path to the current JSON node. + protected override Color ReadString(string str, string path) + { + string[] parts = str.Split(','); + if (parts.Length != 4) + throw new SParseException($"Can't parse {typeof(Color).Name} from invalid value '{str}' (path: {path})."); + + int r = Convert.ToInt32(parts[0]); + int g = Convert.ToInt32(parts[1]); + int b = Convert.ToInt32(parts[2]); + int a = Convert.ToInt32(parts[3]); + return new Color(r, g, b, a); + } + } +} diff --git a/src/SMAPI/Framework/Serialization/PointConverter.cs b/src/SMAPI/Framework/Serialization/PointConverter.cs new file mode 100644 index 00000000..8c2f3396 --- /dev/null +++ b/src/SMAPI/Framework/Serialization/PointConverter.cs @@ -0,0 +1,43 @@ +using System; +using Microsoft.Xna.Framework; +using Newtonsoft.Json.Linq; +using StardewModdingAPI.Toolkit.Serialization; +using StardewModdingAPI.Toolkit.Serialization.Converters; + +namespace StardewModdingAPI.Framework.Serialization +{ + /// Handles deserialization of for crossplatform compatibility. + /// + /// - Linux/Mac format: { "X": 1, "Y": 2 } + /// - Windows format: "1, 2" + /// + internal class PointConverter : SimpleReadOnlyConverter + { + /********* + ** Protected methods + *********/ + /// Read a JSON object. + /// The JSON object to read. + /// The path to the current JSON node. + protected override Point ReadObject(JObject obj, string path) + { + int x = obj.ValueIgnoreCase(nameof(Point.X)); + int y = obj.ValueIgnoreCase(nameof(Point.Y)); + return new Point(x, y); + } + + /// Read a JSON string. + /// The JSON string value. + /// The path to the current JSON node. + protected override Point ReadString(string str, string path) + { + string[] parts = str.Split(','); + if (parts.Length != 2) + throw new SParseException($"Can't parse {typeof(Point).Name} from invalid value '{str}' (path: {path})."); + + int x = Convert.ToInt32(parts[0]); + int y = Convert.ToInt32(parts[1]); + return new Point(x, y); + } + } +} diff --git a/src/SMAPI/Framework/Serialization/RectangleConverter.cs b/src/SMAPI/Framework/Serialization/RectangleConverter.cs new file mode 100644 index 00000000..fbb2e253 --- /dev/null +++ b/src/SMAPI/Framework/Serialization/RectangleConverter.cs @@ -0,0 +1,52 @@ +using System; +using System.Text.RegularExpressions; +using Microsoft.Xna.Framework; +using Newtonsoft.Json.Linq; +using StardewModdingAPI.Toolkit.Serialization; +using StardewModdingAPI.Toolkit.Serialization.Converters; + +namespace StardewModdingAPI.Framework.Serialization +{ + /// Handles deserialization of for crossplatform compatibility. + /// + /// - Linux/Mac format: { "X": 1, "Y": 2, "Width": 3, "Height": 4 } + /// - Windows format: "{X:1 Y:2 Width:3 Height:4}" + /// + internal class RectangleConverter : SimpleReadOnlyConverter + { + /********* + ** Protected methods + *********/ + /// Read a JSON object. + /// The JSON object to read. + /// The path to the current JSON node. + protected override Rectangle ReadObject(JObject obj, string path) + { + int x = obj.ValueIgnoreCase(nameof(Rectangle.X)); + int y = obj.ValueIgnoreCase(nameof(Rectangle.Y)); + int width = obj.ValueIgnoreCase(nameof(Rectangle.Width)); + int height = obj.ValueIgnoreCase(nameof(Rectangle.Height)); + return new Rectangle(x, y, width, height); + } + + /// Read a JSON string. + /// The JSON string value. + /// The path to the current JSON node. + protected override Rectangle ReadString(string str, string path) + { + if (string.IsNullOrWhiteSpace(str)) + return Rectangle.Empty; + + var match = Regex.Match(str, @"^\{X:(?\d+) Y:(?\d+) Width:(?\d+) Height:(?\d+)\}$", RegexOptions.IgnoreCase); + if (!match.Success) + throw new SParseException($"Can't parse {typeof(Rectangle).Name} from invalid value '{str}' (path: {path})."); + + int x = Convert.ToInt32(match.Groups["x"].Value); + int y = Convert.ToInt32(match.Groups["y"].Value); + int width = Convert.ToInt32(match.Groups["width"].Value); + int height = Convert.ToInt32(match.Groups["height"].Value); + + return new Rectangle(x, y, width, height); + } + } +} diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableListWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableListWatcher.cs index 6550f950..32ec8c7e 100644 --- a/src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableListWatcher.cs +++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableListWatcher.cs @@ -53,7 +53,7 @@ namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers { this.AssertNotDisposed(); - // optimise for zero items + // optimize for zero items if (this.CurrentValues.Count == 0) { if (this.LastValues.Count > 0) diff --git a/src/SMAPI/IAssetInfo.cs b/src/SMAPI/IAssetInfo.cs index 5dd58e2e..6cdf01ee 100644 --- a/src/SMAPI/IAssetInfo.cs +++ b/src/SMAPI/IAssetInfo.cs @@ -8,10 +8,10 @@ namespace StardewModdingAPI /********* ** Accessors *********/ - /// The content's locale code, if the content is localised. + /// The content's locale code, if the content is localized. string Locale { get; } - /// The normalised asset name being read. The format may change between platforms; see to compare with a known path. + /// The normalized asset name being read. The format may change between platforms; see to compare with a known path. string AssetName { get; } /// The content data type. @@ -21,7 +21,7 @@ namespace StardewModdingAPI /********* ** Public methods *********/ - /// Get whether the asset name being loaded matches a given name after normalisation. + /// Get whether the asset name being loaded matches a given name after normalization. /// The expected asset path, relative to the game's content folder and without the .xnb extension or locale suffix (like 'Data\ObjectInformation'). bool AssetNameEquals(string path); } diff --git a/src/SMAPI/IContentHelper.cs b/src/SMAPI/IContentHelper.cs index 1b87183d..dd7eb758 100644 --- a/src/SMAPI/IContentHelper.cs +++ b/src/SMAPI/IContentHelper.cs @@ -38,10 +38,10 @@ namespace StardewModdingAPI /// The content asset couldn't be loaded (e.g. because it doesn't exist). T Load(string key, ContentSource source = ContentSource.ModFolder); - /// Normalise an asset name so it's consistent with those generated by the game. This is mainly useful for string comparisons like on generated asset names, and isn't necessary when passing asset names into other content helper methods. + /// Normalize an asset name so it's consistent with those generated by the game. This is mainly useful for string comparisons like on generated asset names, and isn't necessary when passing asset names into other content helper methods. /// The asset key. [Pure] - string NormaliseAssetName(string assetName); + string NormalizeAssetName(string assetName); /// Get the underlying key in the game's content cache for an asset. This can be used to load custom map tilesheets, but should be avoided when you can use the content API instead. This does not validate whether the asset exists. /// The asset key to fetch (if the is ), or the local path to a content file relative to the mod folder. diff --git a/src/SMAPI/IContentPack.cs b/src/SMAPI/IContentPack.cs index 7085c538..c0479eae 100644 --- a/src/SMAPI/IContentPack.cs +++ b/src/SMAPI/IContentPack.cs @@ -31,7 +31,7 @@ namespace StardewModdingAPI /// Read a JSON file from the content pack folder. /// The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types. /// The file path relative to the content pack directory. - /// Returns the deserialised model, or null if the file doesn't exist or is empty. + /// Returns the deserialized model, or null if the file doesn't exist or is empty. /// The is not relative or contains directory climbing (../). TModel ReadJsonFile(string path) where TModel : class; diff --git a/src/SMAPI/IContentPackHelper.cs b/src/SMAPI/IContentPackHelper.cs index e4949f58..c48a4f86 100644 --- a/src/SMAPI/IContentPackHelper.cs +++ b/src/SMAPI/IContentPackHelper.cs @@ -11,7 +11,7 @@ namespace StardewModdingAPI /// Get all content packs loaded for this mod. IEnumerable GetOwned(); - /// Create a temporary content pack to read files from a directory, using randomised manifest fields. Temporary content packs will not appear in the SMAPI log and update checks will not be performed. + /// Create a temporary content pack to read files from a directory, using randomized manifest fields. Temporary content packs will not appear in the SMAPI log and update checks will not be performed. /// The absolute directory path containing the content pack files. IContentPack CreateFake(string directoryPath); diff --git a/src/SMAPI/IDataHelper.cs b/src/SMAPI/IDataHelper.cs index 6afdc529..252030bd 100644 --- a/src/SMAPI/IDataHelper.cs +++ b/src/SMAPI/IDataHelper.cs @@ -14,7 +14,7 @@ namespace StardewModdingAPI /// Read data from a JSON file in the mod's folder. /// The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types. /// The file path relative to the mod folder. - /// Returns the deserialised model, or null if the file doesn't exist or is empty. + /// Returns the deserialized model, or null if the file doesn't exist or is empty. /// The is not relative or contains directory climbing (../). TModel ReadJsonFile(string path) where TModel : class; diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 3fbca04a..b72590fd 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -24,8 +24,8 @@ namespace StardewModdingAPI.Metadata /********* ** Fields *********/ - /// Normalises an asset key to match the cache key. - private readonly Func GetNormalisedPath; + /// Normalizes an asset key to match the cache key. + private readonly Func GetNormalizedPath; /// Simplifies access to private game code. private readonly Reflector Reflection; @@ -33,7 +33,7 @@ namespace StardewModdingAPI.Metadata /// Encapsulates monitoring and logging. private readonly IMonitor Monitor; - /// Optimised bucket categories for batch reloading assets. + /// Optimized bucket categories for batch reloading assets. private enum AssetBucket { /// NPC overworld sprites. @@ -50,13 +50,13 @@ namespace StardewModdingAPI.Metadata /********* ** Public methods *********/ - /// Initialise the core asset data. - /// Normalises an asset key to match the cache key. + /// Initialize the core asset data. + /// Normalizes an asset key to match the cache key. /// Simplifies access to private code. /// Encapsulates monitoring and logging. - public CoreAssetPropagator(Func getNormalisedPath, Reflector reflection, IMonitor monitor) + public CoreAssetPropagator(Func getNormalizedPath, Reflector reflection, IMonitor monitor) { - this.GetNormalisedPath = getNormalisedPath; + this.GetNormalizedPath = getNormalizedPath; this.Reflection = reflection; this.Monitor = monitor; } @@ -67,7 +67,7 @@ namespace StardewModdingAPI.Metadata /// Returns the number of reloaded assets. public int Propagate(LocalizedContentManager content, IDictionary assets) { - // group into optimised lists + // group into optimized lists var buckets = assets.GroupBy(p => { if (this.IsInFolder(p.Key, "Characters") || this.IsInFolder(p.Key, "Characters\\Monsters")) @@ -112,7 +112,7 @@ namespace StardewModdingAPI.Metadata /// Returns whether an asset was loaded. The return value may be true or false, or a non-null value for true. private bool PropagateOther(LocalizedContentManager content, string key, Type type) { - key = this.GetNormalisedPath(key); + key = this.GetNormalizedPath(key); /**** ** Special case: current map tilesheet @@ -123,7 +123,7 @@ namespace StardewModdingAPI.Metadata { foreach (TileSheet tilesheet in Game1.currentLocation.map.TileSheets) { - if (this.GetNormalisedPath(tilesheet.ImageSource) == key) + if (this.GetNormalizedPath(tilesheet.ImageSource) == key) Game1.mapDisplayDevice.LoadTileSheet(tilesheet); } } @@ -136,7 +136,7 @@ namespace StardewModdingAPI.Metadata bool anyChanged = false; foreach (GameLocation location in this.GetLocations()) { - if (!string.IsNullOrWhiteSpace(location.mapPath.Value) && this.GetNormalisedPath(location.mapPath.Value) == key) + if (!string.IsNullOrWhiteSpace(location.mapPath.Value) && this.GetNormalizedPath(location.mapPath.Value) == key) { // general updates location.reloadMap(); @@ -162,7 +162,7 @@ namespace StardewModdingAPI.Metadata ** Propagate by key ****/ Reflector reflection = this.Reflection; - switch (key.ToLower().Replace("/", "\\")) // normalised key so we can compare statically + switch (key.ToLower().Replace("/", "\\")) // normalized key so we can compare statically { /**** ** Animals @@ -584,7 +584,7 @@ namespace StardewModdingAPI.Metadata let locCritters = this.Reflection.GetField>(location, "critters").GetValue() where locCritters != null from Critter critter in locCritters - where this.GetNormalisedPath(critter.sprite.textureName) == key + where this.GetNormalizedPath(critter.sprite.textureName) == key select critter ) .ToArray(); @@ -664,7 +664,7 @@ namespace StardewModdingAPI.Metadata // get NPCs HashSet lookup = new HashSet(keys, StringComparer.InvariantCultureIgnoreCase); NPC[] characters = this.GetCharacters() - .Where(npc => npc.Sprite != null && lookup.Contains(this.GetNormalisedPath(npc.Sprite.textureName.Value))) + .Where(npc => npc.Sprite != null && lookup.Contains(this.GetNormalizedPath(npc.Sprite.textureName.Value))) .ToArray(); if (!characters.Any()) return 0; @@ -692,7 +692,7 @@ namespace StardewModdingAPI.Metadata ( from npc in this.GetCharacters() where npc.isVillager() - let textureKey = this.GetNormalisedPath($"Portraits\\{this.getTextureName(npc)}") + let textureKey = this.GetNormalizedPath($"Portraits\\{this.getTextureName(npc)}") where lookup.Contains(textureKey) select new { npc, textureKey } ) @@ -852,17 +852,17 @@ namespace StardewModdingAPI.Metadata } } - /// Get whether a key starts with a substring after the substring is normalised. + /// Get whether a key starts with a substring after the substring is normalized. /// The key to check. - /// The substring to normalise and find. + /// The substring to normalize and find. private bool KeyStartsWith(string key, string rawSubstring) { - return key.StartsWith(this.GetNormalisedPath(rawSubstring), StringComparison.InvariantCultureIgnoreCase); + return key.StartsWith(this.GetNormalizedPath(rawSubstring), StringComparison.InvariantCultureIgnoreCase); } - /// 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. + /// Get whether a normalized asset key is in the given folder. + /// The normalized asset key (like Animals/cat). + /// The key folder (like Animals); doesn't need to be normalized. /// Whether to return true if the key is inside a subfolder of the . private bool IsInFolder(string key, string folder, bool allowSubfolders = false) { diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index 72410d41..95482708 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -48,11 +48,11 @@ namespace StardewModdingAPI.Metadata ****/ yield return new TypeFinder("Harmony.HarmonyInstance", InstructionHandleResult.DetectedGamePatch); yield return new TypeFinder("System.Runtime.CompilerServices.CallSite", InstructionHandleResult.DetectedDynamic); - yield return new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.serializer), InstructionHandleResult.DetectedSaveSerialiser); - yield return new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.farmerSerializer), InstructionHandleResult.DetectedSaveSerialiser); - yield return new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.locationSerializer), InstructionHandleResult.DetectedSaveSerialiser); - yield return new EventFinder(typeof(ISpecialisedEvents).FullName, nameof(ISpecialisedEvents.UnvalidatedUpdateTicked), InstructionHandleResult.DetectedUnvalidatedUpdateTick); - yield return new EventFinder(typeof(ISpecialisedEvents).FullName, nameof(ISpecialisedEvents.UnvalidatedUpdateTicking), InstructionHandleResult.DetectedUnvalidatedUpdateTick); + yield return new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.serializer), InstructionHandleResult.DetectedSaveSerializer); + yield return new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.farmerSerializer), InstructionHandleResult.DetectedSaveSerializer); + yield return new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.locationSerializer), InstructionHandleResult.DetectedSaveSerializer); + yield return new EventFinder(typeof(ISpecializedEvents).FullName, nameof(ISpecializedEvents.UnvalidatedUpdateTicked), InstructionHandleResult.DetectedUnvalidatedUpdateTick); + yield return new EventFinder(typeof(ISpecializedEvents).FullName, nameof(ISpecializedEvents.UnvalidatedUpdateTicking), InstructionHandleResult.DetectedUnvalidatedUpdateTick); /**** ** detect paranoid issues diff --git a/src/SMAPI/Mod.cs b/src/SMAPI/Mod.cs index 3a753afc..0e5be1c1 100644 --- a/src/SMAPI/Mod.cs +++ b/src/SMAPI/Mod.cs @@ -41,7 +41,7 @@ namespace StardewModdingAPI ** Private methods *********/ /// Release or reset unmanaged resources when the game exits. There's no guarantee this will be called on every exit. - /// Whether the instance is being disposed explicitly rather than finalised. If this is false, the instance shouldn't dispose other objects since they may already be finalised. + /// Whether the instance is being disposed explicitly rather than finalized. If this is false, the instance shouldn't dispose other objects since they may already be finalized. protected virtual void Dispose(bool disposing) { } /// Destruct the instance. diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index df7654cd..6c94a2af 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -39,7 +39,7 @@ namespace StardewModdingAPI } catch (Exception ex) { - Console.WriteLine($"SMAPI failed to initialise: {ex}"); + Console.WriteLine($"SMAPI failed to initialize: {ex}"); Program.PressAnyKeyToExit(true); } } @@ -108,7 +108,7 @@ namespace StardewModdingAPI } - /// Initialise SMAPI and launch the game. + /// Initialize SMAPI and launch the game. /// The command-line arguments. /// This method is separate from because that can't contain any references to assemblies loaded by (e.g. via ), or Mono will incorrectly show an assembly resolution error before assembly resolution is set up. private static void Start(string[] args) diff --git a/src/SMAPI/SemanticVersion.cs b/src/SMAPI/SemanticVersion.cs index b346cfdd..0db41673 100644 --- a/src/SMAPI/SemanticVersion.cs +++ b/src/SMAPI/SemanticVersion.cs @@ -61,13 +61,13 @@ namespace StardewModdingAPI this.Version = version; } - /// Whether this is a pre-release version. + /// Whether this is a prerelease version. public bool IsPrerelease() { return this.Version.IsPrerelease(); } - /// Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version. + /// Get an integer indicating whether this version precedes (less than 0), supersedes (more than 0), or is equivalent to (0) the specified version. /// The version to compare with this instance. /// The value is null. /// The implementation is defined by Semantic Version 2.0 (https://semver.org/). diff --git a/src/SMAPI/Translation.cs b/src/SMAPI/Translation.cs index abcdb336..e8698e2c 100644 --- a/src/SMAPI/Translation.cs +++ b/src/SMAPI/Translation.cs @@ -38,7 +38,7 @@ namespace StardewModdingAPI /********* ** Public methods *********/ - /// Construct an isntance. + /// Construct an instance. /// The name of the relevant mod for error messages. /// The locale for which the translation was fetched. /// The translation key. @@ -46,7 +46,7 @@ namespace StardewModdingAPI internal Translation(string modName, string locale, string key, string text) : this(modName, locale, key, text, string.Format(Translation.PlaceholderText, key)) { } - /// Construct an isntance. + /// Construct an instance. /// The name of the relevant mod for error messages. /// The locale for which the translation was fetched. /// The translation key. diff --git a/src/SMAPI/Utilities/SDate.cs b/src/SMAPI/Utilities/SDate.cs index 9ea4f370..0ab37aa0 100644 --- a/src/SMAPI/Utilities/SDate.cs +++ b/src/SMAPI/Utilities/SDate.cs @@ -197,7 +197,7 @@ namespace StardewModdingAPI.Utilities if (year < 1) throw new ArgumentException($"Invalid year '{year}', must be at least 1."); - // initialise + // initialize this.Day = day; this.Season = season; this.Year = year; @@ -256,12 +256,12 @@ namespace StardewModdingAPI.Utilities /// Get a season index. /// The season name. - /// The current season wasn't recognised. + /// The current season wasn't recognized. private int GetSeasonIndex(string season) { int index = Array.IndexOf(this.Seasons, season); if (index == -1) - throw new InvalidOperationException($"The season '{season}' wasn't recognised."); + throw new InvalidOperationException($"The season '{season}' wasn't recognized."); return index; } } -- cgit From 8b09a2776d9c0faf96fa90c923952033ce659477 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 7 Nov 2019 13:51:45 -0500 Subject: add support for CurseForge update keys (#605) --- docs/release-notes.md | 1 + .../Framework/UpdateData/ModRepositoryKey.cs | 3 + src/SMAPI.Web/Controllers/ModsApiController.cs | 5 +- .../Clients/CurseForge/CurseForgeClient.cs | 113 +++++++++++++++++++++ .../Framework/Clients/CurseForge/CurseForgeMod.cs | 23 +++++ .../Clients/CurseForge/ICurseForgeClient.cs | 17 ++++ .../CurseForge/ResponseModels/ModFileModel.cs | 12 +++ .../Clients/CurseForge/ResponseModels/ModModel.cs | 18 ++++ .../Framework/ConfigModels/ApiClientsConfig.cs | 7 ++ .../ModRepositories/CurseForgeRepository.cs | 63 ++++++++++++ src/SMAPI.Web/Startup.cs | 5 + src/SMAPI.Web/appsettings.json | 2 + 12 files changed, 268 insertions(+), 1 deletion(-) create mode 100644 src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs create mode 100644 src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeMod.cs create mode 100644 src/SMAPI.Web/Framework/Clients/CurseForge/ICurseForgeClient.cs create mode 100644 src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModFileModel.cs create mode 100644 src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModModel.cs create mode 100644 src/SMAPI.Web/Framework/ModRepositories/CurseForgeRepository.cs (limited to 'src/SMAPI.Web/Framework') diff --git a/docs/release-notes.md b/docs/release-notes.md index 1d933f96..5c12c4cc 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -47,6 +47,7 @@ For modders: * Now ignores metadata files and folders (like `__MACOSX` and `__folder_managed_by_vortex`) and content files (like `.txt` or `.png`), which avoids missing-manifest errors in some common cases. * Now detects XNB mods more accurately, and consolidates multi-folder XNB mods in logged messages. * SMAPI now automatically removes invalid content when loading a save to prevent crashes. A warning is shown in-game when this happens. This applies for locations and NPCs. + * Added update checks for CurseForge mods. * Added support for configuring console colors via `smapi-internal/config.json` (intended for players with unusual consoles). * Added support for specifying SMAPI command-line arguments as environment variables for Linux/Mac compatibility. * Improved launch script compatibility on Linux (thanks to kurumushi and toastal!). diff --git a/src/SMAPI.Toolkit/Framework/UpdateData/ModRepositoryKey.cs b/src/SMAPI.Toolkit/Framework/UpdateData/ModRepositoryKey.cs index f6c402d5..765ca334 100644 --- a/src/SMAPI.Toolkit/Framework/UpdateData/ModRepositoryKey.cs +++ b/src/SMAPI.Toolkit/Framework/UpdateData/ModRepositoryKey.cs @@ -9,6 +9,9 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData /// The Chucklefish mod repository. Chucklefish, + /// The CurseForge mod repository. + CurseForge, + /// A GitHub project containing releases. GitHub, diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 8419b220..1412105a 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -14,6 +14,7 @@ using StardewModdingAPI.Toolkit.Framework.UpdateData; using StardewModdingAPI.Web.Framework.Caching.Mods; using StardewModdingAPI.Web.Framework.Caching.Wiki; using StardewModdingAPI.Web.Framework.Clients.Chucklefish; +using StardewModdingAPI.Web.Framework.Clients.CurseForge; using StardewModdingAPI.Web.Framework.Clients.GitHub; using StardewModdingAPI.Web.Framework.Clients.ModDrop; using StardewModdingAPI.Web.Framework.Clients.Nexus; @@ -61,10 +62,11 @@ namespace StardewModdingAPI.Web.Controllers /// The cache in which to store mod metadata. /// The config settings for mod update checks. /// The Chucklefish API client. + /// The CurseForge API client. /// The GitHub API client. /// The ModDrop API client. /// The Nexus API client. - public ModsApiController(IHostingEnvironment environment, IWikiCacheRepository wikiCache, IModCacheRepository modCache, IOptions configProvider, IChucklefishClient chucklefish, IGitHubClient github, IModDropClient modDrop, INexusClient nexus) + public ModsApiController(IHostingEnvironment environment, IWikiCacheRepository wikiCache, IModCacheRepository modCache, IOptions configProvider, IChucklefishClient chucklefish, ICurseForgeClient curseForge, IGitHubClient github, IModDropClient modDrop, INexusClient nexus) { this.ModDatabase = new ModToolkit().GetModDatabase(Path.Combine(environment.WebRootPath, "SMAPI.metadata.json")); ModUpdateCheckConfig config = configProvider.Value; @@ -78,6 +80,7 @@ namespace StardewModdingAPI.Web.Controllers new IModRepository[] { new ChucklefishRepository(chucklefish), + new CurseForgeRepository(curseForge), new GitHubRepository(github), new ModDropRepository(modDrop), new NexusRepository(nexus) diff --git a/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs b/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs new file mode 100644 index 00000000..140b854e --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs @@ -0,0 +1,113 @@ +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Pathoschild.Http.Client; +using StardewModdingAPI.Toolkit; +using StardewModdingAPI.Web.Framework.Clients.CurseForge.ResponseModels; + +namespace StardewModdingAPI.Web.Framework.Clients.CurseForge +{ + /// An HTTP client for fetching mod metadata from the CurseForge API. + internal class CurseForgeClient : ICurseForgeClient + { + /********* + ** Fields + *********/ + /// The underlying HTTP client. + private readonly IClient Client; + + /// A regex pattern which matches a version number in a CurseForge mod file name. + private readonly Regex VersionInNamePattern = new Regex(@"^(?:.+? | *)v?(\d+\.\d+(?:\.\d+)?(?:-.+?)?) *(?:\.(?:zip|rar|7z))?$", RegexOptions.Compiled); + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The user agent for the API client. + /// The base URL for the CurseForge API. + public CurseForgeClient(string userAgent, string apiUrl) + { + this.Client = new FluentClient(apiUrl).SetUserAgent(userAgent); + } + + /// Get metadata about a mod. + /// The CurseForge mod ID. + /// Returns the mod info if found, else null. + public async Task GetModAsync(long id) + { + // get raw data + ModModel mod = await this.Client + .GetAsync($"addon/{id}") + .As(); + if (mod == null) + return null; + + // get latest versions + string invalidVersion = null; + ISemanticVersion latest = null; + foreach (ModFileModel file in mod.LatestFiles) + { + // extract version + ISemanticVersion version; + { + string raw = this.GetRawVersion(file); + if (raw == null) + continue; + + if (!SemanticVersion.TryParse(raw, out version)) + { + if (invalidVersion == null) + invalidVersion = raw; + continue; + } + } + + // track latest version + if (latest == null || version.IsNewerThan(latest)) + latest = version; + } + + // get error + string error = null; + if (latest == null && invalidVersion == null) + { + error = mod.LatestFiles.Any() + ? $"CurseForge mod {id} has no downloads which specify the version in a recognised format." + : $"CurseForge mod {id} has no downloads."; + } + + // generate result + return new CurseForgeMod + { + Name = mod.Name, + LatestVersion = latest?.ToString() ?? invalidVersion, + Url = mod.WebsiteUrl, + Error = error + }; + } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public void Dispose() + { + this.Client?.Dispose(); + } + + + /********* + ** Private methods + *********/ + /// Get a raw version string for a mod file, if available. + /// The file whose version to get. + private string GetRawVersion(ModFileModel file) + { + Match match = this.VersionInNamePattern.Match(file.DisplayName); + if (!match.Success) + match = this.VersionInNamePattern.Match(file.FileName); + + return match.Success + ? match.Groups[1].Value + : null; + } + } +} diff --git a/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeMod.cs b/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeMod.cs new file mode 100644 index 00000000..e5bb8cf1 --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeMod.cs @@ -0,0 +1,23 @@ +using Newtonsoft.Json; + +namespace StardewModdingAPI.Web.Framework.Clients.CurseForge +{ + /// Mod metadata from the CurseForge API. + internal class CurseForgeMod + { + /********* + ** Accessors + *********/ + /// The mod name. + public string Name { get; set; } + + /// The latest file version. + public string LatestVersion { get; set; } + + /// The mod's web URL. + public string Url { get; set; } + + /// A user-friendly error which indicates why fetching the mod info failed (if applicable). + public string Error { get; set; } + } +} diff --git a/src/SMAPI.Web/Framework/Clients/CurseForge/ICurseForgeClient.cs b/src/SMAPI.Web/Framework/Clients/CurseForge/ICurseForgeClient.cs new file mode 100644 index 00000000..907b4087 --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/CurseForge/ICurseForgeClient.cs @@ -0,0 +1,17 @@ +using System; +using System.Threading.Tasks; + +namespace StardewModdingAPI.Web.Framework.Clients.CurseForge +{ + /// An HTTP client for fetching mod metadata from the CurseForge API. + internal interface ICurseForgeClient : IDisposable + { + /********* + ** Methods + *********/ + /// Get metadata about a mod. + /// The CurseForge mod ID. + /// Returns the mod info if found, else null. + Task GetModAsync(long id); + } +} diff --git a/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModFileModel.cs b/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModFileModel.cs new file mode 100644 index 00000000..9de74847 --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModFileModel.cs @@ -0,0 +1,12 @@ +namespace StardewModdingAPI.Web.Framework.Clients.CurseForge.ResponseModels +{ + /// Metadata from the CurseForge API about a mod file. + public class ModFileModel + { + /// The file name as downloaded. + public string FileName { get; set; } + + /// The file display name. + public string DisplayName { get; set; } + } +} diff --git a/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModModel.cs b/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModModel.cs new file mode 100644 index 00000000..48cd185b --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/CurseForge/ResponseModels/ModModel.cs @@ -0,0 +1,18 @@ +namespace StardewModdingAPI.Web.Framework.Clients.CurseForge.ResponseModels +{ + /// An mod from the CurseForge API. + public class ModModel + { + /// The mod's unique ID on CurseForge. + public int ID { get; set; } + + /// The mod name. + public string Name { get; set; } + + /// The web URL for the mod page. + public string WebsiteUrl { get; set; } + + /// The available file downloads. + public ModFileModel[] LatestFiles { get; set; } + } +} diff --git a/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs index a0a1f42a..121690c5 100644 --- a/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs +++ b/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs @@ -23,6 +23,13 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels public string ChucklefishModPageUrlFormat { get; set; } + /**** + ** CurseForge + ****/ + /// The base URL for the CurseForge API. + public string CurseForgeBaseUrl { get; set; } + + /**** ** GitHub ****/ diff --git a/src/SMAPI.Web/Framework/ModRepositories/CurseForgeRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/CurseForgeRepository.cs new file mode 100644 index 00000000..93ddc1eb --- /dev/null +++ b/src/SMAPI.Web/Framework/ModRepositories/CurseForgeRepository.cs @@ -0,0 +1,63 @@ +using System; +using System.Threading.Tasks; +using StardewModdingAPI.Toolkit.Framework.UpdateData; +using StardewModdingAPI.Web.Framework.Clients.CurseForge; + +namespace StardewModdingAPI.Web.Framework.ModRepositories +{ + /// An HTTP client for fetching mod metadata from CurseForge. + internal class CurseForgeRepository : RepositoryBase + { + /********* + ** Fields + *********/ + /// The underlying CurseForge API client. + private readonly ICurseForgeClient Client; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The underlying CurseForge API client. + public CurseForgeRepository(ICurseForgeClient client) + : base(ModRepositoryKey.CurseForge) + { + this.Client = client; + } + + /// Get metadata about a mod in the repository. + /// The mod ID in this repository. + public override async Task GetModInfoAsync(string id) + { + // validate ID format + if (!uint.TryParse(id, out uint curseID)) + return new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid CurseForge mod ID, must be an integer ID."); + + // fetch info + try + { + CurseForgeMod mod = await this.Client.GetModAsync(curseID); + if (mod == null) + return new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, "Found no CurseForge mod with this ID."); + if (mod.Error != null) + { + RemoteModStatus remoteStatus = RemoteModStatus.InvalidData; + return new ModInfoModel().SetError(remoteStatus, mod.Error); + } + + return new ModInfoModel(name: mod.Name, version: this.NormalizeVersion(mod.LatestVersion), url: mod.Url); + } + catch (Exception ex) + { + return new ModInfoModel().SetError(RemoteModStatus.TemporaryError, ex.ToString()); + } + } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public override void Dispose() + { + this.Client.Dispose(); + } + } +} diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index bf69d543..8110b696 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -16,6 +16,7 @@ using StardewModdingAPI.Web.Framework.Caching; using StardewModdingAPI.Web.Framework.Caching.Mods; using StardewModdingAPI.Web.Framework.Caching.Wiki; using StardewModdingAPI.Web.Framework.Clients.Chucklefish; +using StardewModdingAPI.Web.Framework.Clients.CurseForge; using StardewModdingAPI.Web.Framework.Clients.GitHub; using StardewModdingAPI.Web.Framework.Clients.ModDrop; using StardewModdingAPI.Web.Framework.Clients.Nexus; @@ -119,6 +120,10 @@ namespace StardewModdingAPI.Web baseUrl: api.ChucklefishBaseUrl, modPageUrlFormat: api.ChucklefishModPageUrlFormat )); + services.AddSingleton(new CurseForgeClient( + userAgent: userAgent, + apiUrl: api.CurseForgeBaseUrl + )); services.AddSingleton(new GitHubClient( baseUrl: api.GitHubBaseUrl, diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json index a440cf42..674bb672 100644 --- a/src/SMAPI.Web/appsettings.json +++ b/src/SMAPI.Web/appsettings.json @@ -30,6 +30,8 @@ "ChucklefishBaseUrl": "https://community.playstarbound.com", "ChucklefishModPageUrlFormat": "resources/{0}", + "CurseForgeBaseUrl": "https://addons-ecs.forgesvc.net/api/v2/", + "GitHubBaseUrl": "https://api.github.com", "GitHubAcceptHeader": "application/vnd.github.v3+json", "GitHubUsername": null, // see top note -- cgit From 0aac0717bf1c46d798ed007d3c7c5c050e07f354 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 8 Nov 2019 13:44:49 -0500 Subject: add CurseForge to mod metadata (#605) --- .../Framework/Clients/WebApi/ModExtendedMetadataModel.cs | 8 ++++++++ src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs | 4 ++++ src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs | 6 ++++++ src/SMAPI.Web/Controllers/ModsApiController.cs | 10 ++++++---- src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs | 10 ++++++++++ src/SMAPI.Web/ViewModels/ModModel.cs | 13 +++++++++---- src/SMAPI.Web/wwwroot/Content/js/mods.js | 3 +++ 7 files changed, 46 insertions(+), 8 deletions(-) (limited to 'src/SMAPI.Web/Framework') diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs index bd5f71a9..8074210c 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs @@ -28,6 +28,12 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// The mod ID in the Chucklefish mod repo. public int? ChucklefishID { get; set; } + /// The mod ID in the CurseForge mod repo. + public int? CurseForgeID { get; set; } + + /// The mod key in the CurseForge mod repo (used in mod page URLs). + public string CurseForgeKey { get; set; } + /// The mod ID in the ModDrop mod repo. public int? ModDropID { get; set; } @@ -87,6 +93,8 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi this.Name = wiki.Name.FirstOrDefault(); this.NexusID = wiki.NexusID; this.ChucklefishID = wiki.ChucklefishID; + this.CurseForgeID = wiki.CurseForgeID; + this.CurseForgeKey = wiki.CurseForgeKey; this.ModDropID = wiki.ModDropID; this.GitHubRepo = wiki.GitHubRepo; this.CustomSourceUrl = wiki.CustomSourceUrl; diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs index ab6a2517..610e14f1 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs @@ -93,6 +93,8 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki string[] warnings = this.GetAttributeAsCsv(node, "data-warnings"); int? nexusID = this.GetAttributeAsNullableInt(node, "data-nexus-id"); int? chucklefishID = this.GetAttributeAsNullableInt(node, "data-cf-id"); + int? curseForgeID = this.GetAttributeAsNullableInt(node, "data-curseforge-id"); + string curseForgeKey = this.GetAttribute(node, "data-curseforge-key"); int? modDropID = this.GetAttributeAsNullableInt(node, "data-moddrop-id"); string githubRepo = this.GetAttribute(node, "data-github"); string customSourceUrl = this.GetAttribute(node, "data-custom-source"); @@ -145,6 +147,8 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki Author = authors, NexusID = nexusID, ChucklefishID = chucklefishID, + CurseForgeID = curseForgeID, + CurseForgeKey = curseForgeKey, ModDropID = modDropID, GitHubRepo = githubRepo, CustomSourceUrl = customSourceUrl, diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs index 06c44308..51bb2336 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs @@ -23,6 +23,12 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki /// The mod ID in the Chucklefish mod repo. public int? ChucklefishID { get; set; } + /// The mod ID in the CurseForge mod repo. + public int? CurseForgeID { get; set; } + + /// The mod key in the CurseForge mod repo (used in mod page URLs). + public string CurseForgeKey { get; set; } + /// The mod ID in the ModDrop mod repo. public int? ModDropID { get; set; } diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 1412105a..f65b164f 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -280,11 +280,13 @@ namespace StardewModdingAPI.Web.Controllers if (entry != null) { if (entry.NexusID.HasValue) - yield return $"Nexus:{entry.NexusID}"; - if (entry.ChucklefishID.HasValue) - yield return $"Chucklefish:{entry.ChucklefishID}"; + yield return $"{ModRepositoryKey.Nexus}:{entry.NexusID}"; if (entry.ModDropID.HasValue) - yield return $"ModDrop:{entry.ModDropID}"; + yield return $"{ModRepositoryKey.ModDrop}:{entry.ModDropID}"; + if (entry.CurseForgeID.HasValue) + yield return $"{ModRepositoryKey.CurseForge}:{entry.CurseForgeID}"; + if (entry.ChucklefishID.HasValue) + yield return $"{ModRepositoryKey.Chucklefish}:{entry.ChucklefishID}"; } } diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs b/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs index 37f55db1..fddf99ee 100644 --- a/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs +++ b/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs @@ -40,6 +40,12 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki /// The mod ID in the Chucklefish mod repo. public int? ChucklefishID { get; set; } + /// The mod ID in the CurseForge mod repo. + public int? CurseForgeID { get; set; } + + /// The mod key in the CurseForge mod repo (used in mod page URLs). + public string CurseForgeKey { get; set; } + /// The mod ID in the ModDrop mod repo. public int? ModDropID { get; set; } @@ -123,6 +129,8 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki this.Author = mod.Author; this.NexusID = mod.NexusID; this.ChucklefishID = mod.ChucklefishID; + this.CurseForgeID = mod.CurseForgeID; + this.CurseForgeKey = mod.CurseForgeKey; this.ModDropID = mod.ModDropID; this.GitHubRepo = mod.GitHubRepo; this.CustomSourceUrl = mod.CustomSourceUrl; @@ -158,6 +166,8 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki Author = this.Author, NexusID = this.NexusID, ChucklefishID = this.ChucklefishID, + CurseForgeID = this.CurseForgeID, + CurseForgeKey = this.CurseForgeKey, ModDropID = this.ModDropID, GitHubRepo = this.GitHubRepo, CustomSourceUrl = this.CustomSourceUrl, diff --git a/src/SMAPI.Web/ViewModels/ModModel.cs b/src/SMAPI.Web/ViewModels/ModModel.cs index 5a343640..2b478c81 100644 --- a/src/SMAPI.Web/ViewModels/ModModel.cs +++ b/src/SMAPI.Web/ViewModels/ModModel.cs @@ -100,15 +100,20 @@ namespace StardewModdingAPI.Web.ViewModels anyFound = true; yield return new ModLinkModel($"https://www.nexusmods.com/stardewvalley/mods/{entry.NexusID}", "Nexus"); } - if (entry.ChucklefishID.HasValue) + if (entry.ModDropID.HasValue) { anyFound = true; - yield return new ModLinkModel($"https://community.playstarbound.com/resources/{entry.ChucklefishID}", "Chucklefish"); + yield return new ModLinkModel($"https://www.moddrop.com/sdv/mod/{entry.ModDropID}", "ModDrop"); } - if (entry.ModDropID.HasValue) + if (!string.IsNullOrWhiteSpace(entry.CurseForgeKey)) { anyFound = true; - yield return new ModLinkModel($"https://www.moddrop.com/sdv/mod/{entry.ModDropID}", "ModDrop"); + yield return new ModLinkModel($"https://www.curseforge.com/stardewvalley/mods/{entry.CurseForgeKey}", "CurseForge"); + } + if (entry.ChucklefishID.HasValue) + { + anyFound = true; + yield return new ModLinkModel($"https://community.playstarbound.com/resources/{entry.ChucklefishID}", "Chucklefish"); } // fallback diff --git a/src/SMAPI.Web/wwwroot/Content/js/mods.js b/src/SMAPI.Web/wwwroot/Content/js/mods.js index 130f60be..0394ac4f 100644 --- a/src/SMAPI.Web/wwwroot/Content/js/mods.js +++ b/src/SMAPI.Web/wwwroot/Content/js/mods.js @@ -44,6 +44,7 @@ smapi.modList = function (mods, enableBeta) { download: { value: { chucklefish: { value: true, label: "Chucklefish" }, + curseforge: { value: true, label: "CurseForge" }, moddrop: { value: true, label: "ModDrop" }, nexus: { value: true, label: "Nexus" }, custom: { value: true } @@ -180,6 +181,8 @@ smapi.modList = function (mods, enableBeta) { if (!filters.download.value.chucklefish.value) ignoreSites.push("Chucklefish"); + if (!filters.download.value.curseforge.value) + ignoreSites.push("CurseForge"); if (!filters.download.value.moddrop.value) ignoreSites.push("ModDrop"); if (!filters.download.value.nexus.value) -- cgit From fd6a719b02d1d45d27509f44f09eefe52124ee20 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 9 Nov 2019 21:18:06 -0500 Subject: overhaul update checks This commit moves the core update-check logic serverside, and adds support for community-defined version mappings. For example, that means false update alerts can now be solved by the community for all players. --- docs/release-notes.md | 4 + docs/technical/web.md | 209 ++++++++++++++++++--- .../Framework/Clients/WebApi/ModEntryModel.cs | 18 +- .../Clients/WebApi/ModExtendedMetadataModel.cs | 24 ++- .../Framework/Clients/WebApi/ModSeachModel.cs | 36 ---- .../Clients/WebApi/ModSearchEntryModel.cs | 11 +- .../Framework/Clients/WebApi/ModSearchModel.cs | 52 +++++ .../Framework/Clients/WebApi/WebApiClient.cs | 8 +- .../Framework/Clients/Wiki/WikiClient.cs | 26 +++ .../Framework/Clients/Wiki/WikiModEntry.cs | 7 + .../Framework/ModData/ModDataModel.cs | 6 - .../Framework/ModData/ModDataRecord.cs | 31 --- .../ModData/ModDataRecordVersionedFields.cs | 24 --- src/SMAPI.Web/Controllers/ModsApiController.cs | 162 +++++++++++++--- .../Framework/Caching/Wiki/CachedWikiMod.cs | 24 ++- src/SMAPI.Web/Views/Index/Privacy.cshtml | 2 +- src/SMAPI.Web/wwwroot/SMAPI.metadata.json | 97 ---------- src/SMAPI/Framework/SCore.cs | 57 ++---- 18 files changed, 493 insertions(+), 305 deletions(-) delete mode 100644 src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs create mode 100644 src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchModel.cs (limited to 'src/SMAPI.Web/Framework') diff --git a/docs/release-notes.md b/docs/release-notes.md index eace8be5..5f643f07 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -18,6 +18,9 @@ For players: * **Improved mod scanning.** SMAPI now supports some non-standard mod structures automatically, improves compatibility with the Vortex mod manager, and improves various error/skip messages related to mod loading. +* **Overhauled update checks.** + SMAPI update checks are now handled entirely on the web server and support community-defined version mappings. For example, that means false update alerts can now be solved by the community for all players. + * **Fixed many bugs and edge cases.** For modders: @@ -50,6 +53,7 @@ For modders: * Added update checks for CurseForge mods. * Added support for configuring console colors via `smapi-internal/config.json` (intended for players with unusual consoles). * Added support for specifying SMAPI command-line arguments as environment variables for Linux/Mac compatibility. + * Overhauled update checks and added community-defined version mapping. * Improved launch script compatibility on Linux (thanks to kurumushi and toastal!). * Made error messages more user-friendly in some cases. * Save Backup now works in the background, to avoid affecting startup time for players with a large number of saves. diff --git a/docs/technical/web.md b/docs/technical/web.md index 719b9433..78d93625 100644 --- a/docs/technical/web.md +++ b/docs/technical/web.md @@ -116,54 +116,201 @@ SMAPI provides a web API at `api.smapi.io` for use by SMAPI and external tools. accessible but not officially released; it may change at any time. ### `/mods` endpoint -The API has one `/mods` endpoint. This provides mod info, including official versions and URLs -(from Chucklefish, GitHub, or Nexus), unofficial versions from the wiki, and optional mod metadata -from the wiki and SMAPI's internal data. This is used by SMAPI to perform update checks, and by -external tools to fetch mod data. +The API has one `/mods` endpoint. This crossreferences the mod against a variety of sources (e.g. +the wiki, Chucklefish, CurseForge, ModDrop, and Nexus) to provide metadata mainly intended for +update checks. -The API accepts a `POST` request with the mods to match, each of which **must** specify an ID and -may _optionally_ specify [update keys](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest#Update_checks). -The API will automatically try to fetch known update keys from the wiki and internal data based on -the given ID. +The API accepts a `POST` request with these fields: -``` -POST https://api.smapi.io/v2.0/mods + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
fieldsummary
mods + +The mods for which to fetch metadata. Included fields: + + +field | summary +----- | ------- +`id` | The unique ID in the mod's `manifest.json`. This is used to crossreference with the wiki, and to index mods in the response. If it's unknown (e.g. you just have an update key), you can use a unique fake ID like `FAKE.Nexus.2400`. +`updateKeys` | _(optional)_ [Update keys](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest#Update_checks) which specify the mod pages to check, in addition to any mod pages linked to the `ID`. +`installedVersion` | _(optional)_ The installed version of the mod. If not specified, the API won't recommend an update. +`isBroken` | _(optional)_ Whether SMAPI failed to load the installed version of the mod, e.g. due to incompatibility. If true, the web API will be more permissive when recommending updates (e.g. allowing a stable → prerelease update). + +
apiVersion + +_(optional)_ The installed version of SMAPI. If not specified, the API won't recommend an update. + +
gameVersion + +_(optional)_ The installed version of Stardew Valley. This may be used to select updates. + +
platform + +_(optional)_ The player's OS (`Android`, `Linux`, `Mac`, or `Windows`). This may be used to select updates. + +
includeExtendedMetadata + +_(optional)_ Whether to include extra metadata that's not needed for SMAPI update checks, but which +may be useful to external tools. + +
+ +Example request: +```js +POST https://api.smapi.io/v3.0/mods { "mods": [ { - "id": "Pathoschild.LookupAnything", - "updateKeys": [ "nexus:541", "chucklefish:4250" ] + "id": "Pathoschild.ContentPatcher", + "updateKeys": [ "nexus:1915" ], + "installedVersion": "1.9.2", + "isBroken": false } ], + "apiVersion": "3.0.0", + "gameVersion": "1.4.0", + "platform": "Windows", "includeExtendedMetadata": true } ``` -The API will automatically aggregate versions and errors. Each mod will include... -* an `id` (matching what you passed in); -* up to three versions: `main` (e.g. 'latest version' field on Nexus), `optional` if newer (e.g. - optional files on Nexus), and `unofficial` if newer (from the wiki); -* `metadata` with mod info crossreferenced from the wiki and internal data (only if you specified - `includeExtendedMetadata: true`); -* and `errors` containing any error messages that occurred while fetching data. - -For example: +Response fields: + + + + + + + + + + + + + + + + + + + + + + + + + + +
fieldsummary
id + +The mod ID you specified in the request. + +
suggestedUpdate + +The update version recommended by the web API, if any. This is based on some internal rules (e.g. +it won't recommend a prerelease update if the player has a working stable version) and context +(e.g. whether the player is in the game beta channel). Choosing an update version yourself isn't +recommended, but you can set `includeExtendedMetadata: true` and check the `metadata` field if you +really want to do that. + +
errors + +Human-readable errors that occurred fetching the version info (e.g. if a mod page has an invalid +version). + +
metadata + +Extra metadata that's not needed for SMAPI update checks but which may be useful to external tools, +if you set `includeExtendedMetadata: true` in the request. Included fields: + +field | summary +----- | ------- +`id` | The known `manifest.json` unique IDs for this mod defined on the wiki, if any. That includes historical versions of the mod. +`name` | The normalised name for this mod based on the crossreferenced sites. +`nexusID` | The mod ID on [Nexus Mods](https://www.nexusmods.com/stardewvalley/), if any. +`chucklefishID` | The mod ID in the [Chucklefish mod repo](https://community.playstarbound.com/resources/categories/stardew-valley.22/), if any. +`curseForgeID` | The mod project ID on [CurseForge](https://www.curseforge.com/stardewvalley), if any. +`curseForgeKey` | The mod key on [CurseForge](https://www.curseforge.com/stardewvalley), if any. This is used in the mod page URL. +`modDropID` | The mod ID on [ModDrop](https://www.moddrop.com/stardew-valley), if any. +`gitHubRepo` | The GitHub repository containing the mod code, if any. Specified in the `Owner/Repo` form. +`customSourceUrl` | The custom URL to the mod code, if any. This is used for mods which aren't stored in a GitHub repo. +`customUrl` | The custom URL to the mod page, if any. This is used for mods which aren't stored on one of the standard mod sites covered by the ID fields. +`main` | The primary mod version, if any. This depends on the mod site, but it's typically either the version of the mod itself or of its latest non-optional download. +`optional` | The latest optional download version, if any. +`unofficial` | The version of the unofficial update defined on the wiki for this mod, if any. +`unofficialForBeta` | Equivalent to `unofficial`, but for beta versions of SMAPI or Stardew Valley. +`hasBetaInfo` | Whether there's an ongoing Stardew Valley or SMAPI beta which may affect update checks. +`compatibilityStatus` | The compatibility status for the mod for the stable version of the game, as defined on the wiki, if any. See [possible values](https://github.com/Pathoschild/SMAPI/blob/develop/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs). +`compatibilitySummary` | The human-readable summary of the mod's compatibility in HTML format, if any. +`brokeIn` | The SMAPI or Stardew Valley version that broke this mod, if any. +`betaCompatibilityStatus`
`betaCompatibilitySummary`
`betaBrokeIn` | Equivalent to the preceding fields, but for beta versions of SMAPI or Stardew Valley. + + +
+ +Example response with `includeExtendedMetadata: false`: +```js +[ + { + "id": "Pathoschild.ContentPatcher", + "suggestedUpdate": { + "version": "1.10.0", + "url": "https://www.nexusmods.com/stardewvalley/mods/1915" + }, + "errors": [] + } +] ``` + +Example response with `includeExtendedMetadata: true`: +```js [ { - "id": "Pathoschild.LookupAnything", - "main": { - "version": "1.19", - "url": "https://www.nexusmods.com/stardewvalley/mods/541" + "id": "Pathoschild.ContentPatcher", + "suggestedUpdate": { + "version": "1.10.0", + "url": "https://www.nexusmods.com/stardewvalley/mods/1915" }, "metadata": { - "id": [ - "Pathoschild.LookupAnything", - "LookupAnything" - ], - "name": "Lookup Anything", - "nexusID": 541, + "id": [ "Pathoschild.ContentPatcher" ], + "name": "Content Patcher", + "nexusID": 1915, + "curseForgeID": 309243, + "curseForgeKey": "content-patcher", + "modDropID": 470174, "gitHubRepo": "Pathoschild/StardewMods", + "main": { + "version": "1.10", + "url": "https://www.nexusmods.com/stardewvalley/mods/1915" + }, + "hasBetaInfo": true, "compatibilityStatus": "Ok", "compatibilitySummary": "✓ use latest version." }, diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs index 8a9c0a25..f1bcfccc 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs @@ -1,3 +1,5 @@ +using System; + namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi { /// Metadata about a mod. @@ -9,23 +11,31 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// The mod's unique ID (if known). public string ID { get; set; } + /// The update version recommended by the web API based on its version update and mapping rules. + public ModEntryVersionModel SuggestedUpdate { get; set; } + + /// Optional extended data which isn't needed for update checks. + public ModExtendedMetadataModel Metadata { get; set; } + /// The main version. + [Obsolete] public ModEntryVersionModel Main { get; set; } /// The latest optional version, if newer than . + [Obsolete] public ModEntryVersionModel Optional { get; set; } /// The latest unofficial version, if newer than and . + [Obsolete] public ModEntryVersionModel Unofficial { get; set; } /// The latest unofficial version for the current Stardew Valley or SMAPI beta, if any (see ). + [Obsolete] public ModEntryVersionModel UnofficialForBeta { get; set; } - /// Optional extended data which isn't needed for update checks. - public ModExtendedMetadataModel Metadata { get; set; } - /// Whether a Stardew Valley or SMAPI beta which affects mod compatibility is in progress. If this is true, should be used for beta versions of SMAPI instead of . - public bool HasBetaInfo { get; set; } + [Obsolete] + public bool? HasBetaInfo { get; set; } /// The errors that occurred while fetching update data. public string[] Errors { get; set; } = new string[0]; diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs index 8074210c..4a697585 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs @@ -46,6 +46,17 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// The custom mod page URL (if applicable). public string CustomUrl { get; set; } + /// The main version. + public ModEntryVersionModel Main { get; set; } + + /// The latest optional version, if newer than . + public ModEntryVersionModel Optional { get; set; } + + /// The latest unofficial version, if newer than and . + public ModEntryVersionModel Unofficial { get; set; } + + /// The latest unofficial version for the current Stardew Valley or SMAPI beta, if any (see ). + public ModEntryVersionModel UnofficialForBeta { get; set; } /**** ** Stable compatibility @@ -60,7 +71,6 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// The game or SMAPI version which broke this mod, if applicable. public string BrokeIn { get; set; } - /**** ** Beta compatibility ****/ @@ -84,8 +94,18 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// Construct an instance. /// The mod metadata from the wiki (if available). /// The mod metadata from SMAPI's internal DB (if available). - public ModExtendedMetadataModel(WikiModEntry wiki, ModDataRecord db) + /// The main version. + /// The latest optional version, if newer than . + /// The latest unofficial version, if newer than and . + /// The latest unofficial version for the current Stardew Valley or SMAPI beta, if any. + public ModExtendedMetadataModel(WikiModEntry wiki, ModDataRecord db, ModEntryVersionModel main, ModEntryVersionModel optional, ModEntryVersionModel unofficial, ModEntryVersionModel unofficialForBeta) { + // versions + this.Main = main; + this.Optional = optional; + this.Unofficial = unofficial; + this.UnofficialForBeta = unofficialForBeta; + // wiki data if (wiki != null) { diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs deleted file mode 100644 index a2eaad16..00000000 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Linq; - -namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi -{ - /// Specifies mods whose update-check info to fetch. - public class ModSearchModel - { - /********* - ** Accessors - *********/ - /// The mods for which to find data. - public ModSearchEntryModel[] Mods { get; set; } - - /// Whether to include extended metadata for each mod. - public bool IncludeExtendedMetadata { get; set; } - - - /********* - ** Public methods - *********/ - /// Construct an empty instance. - public ModSearchModel() - { - // needed for JSON deserializing - } - - /// Construct an instance. - /// The mods to search. - /// Whether to include extended metadata for each mod. - public ModSearchModel(ModSearchEntryModel[] mods, bool includeExtendedMetadata) - { - this.Mods = mods.ToArray(); - this.IncludeExtendedMetadata = includeExtendedMetadata; - } - } -} diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs index 886cd5a1..bf81e102 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs @@ -12,6 +12,12 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// The namespaced mod update keys (if available). public string[] UpdateKeys { get; set; } + /// The mod version installed by the local player. This is used for version mapping in some cases. + public ISemanticVersion InstalledVersion { get; set; } + + /// Whether the installed version is broken or could not be loaded. + public bool IsBroken { get; set; } + /********* ** Public methods @@ -24,10 +30,13 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// Construct an instance. /// The unique mod ID. + /// The version installed by the local player. This is used for version mapping in some cases. /// The namespaced mod update keys (if available). - public ModSearchEntryModel(string id, string[] updateKeys) + /// Whether the installed version is broken or could not be loaded. + public ModSearchEntryModel(string id, ISemanticVersion installedVersion, string[] updateKeys, bool isBroken = false) { this.ID = id; + this.InstalledVersion = installedVersion; this.UpdateKeys = updateKeys ?? new string[0]; } } diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchModel.cs new file mode 100644 index 00000000..73698173 --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchModel.cs @@ -0,0 +1,52 @@ +using System.Linq; +using StardewModdingAPI.Toolkit.Utilities; + +namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi +{ + /// Specifies mods whose update-check info to fetch. + public class ModSearchModel + { + /********* + ** Accessors + *********/ + /// The mods for which to find data. + public ModSearchEntryModel[] Mods { get; set; } + + /// Whether to include extended metadata for each mod. + public bool IncludeExtendedMetadata { get; set; } + + /// The SMAPI version installed by the player. This is used for version mapping in some cases. + public ISemanticVersion ApiVersion { get; set; } + + /// The Stardew Valley version installed by the player. + public ISemanticVersion GameVersion { get; set; } + + /// The OS on which the player plays. + public Platform? Platform { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an empty instance. + public ModSearchModel() + { + // needed for JSON deserializing + } + + /// Construct an instance. + /// The mods to search. + /// The SMAPI version installed by the player. If this is null, the API won't provide a recommended update. + /// The Stardew Valley version installed by the player. + /// The OS on which the player plays. + /// Whether to include extended metadata for each mod. + public ModSearchModel(ModSearchEntryModel[] mods, ISemanticVersion apiVersion, ISemanticVersion gameVersion, Platform platform, bool includeExtendedMetadata) + { + this.Mods = mods.ToArray(); + this.ApiVersion = apiVersion; + this.GameVersion = gameVersion; + this.Platform = platform; + this.IncludeExtendedMetadata = includeExtendedMetadata; + } + } +} diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs index 80c8f62b..f0a7c82a 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Net; using Newtonsoft.Json; using StardewModdingAPI.Toolkit.Serialization; +using StardewModdingAPI.Toolkit.Utilities; namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi { @@ -37,12 +38,15 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// Get metadata about a set of mods from the web API. /// The mod keys for which to fetch the latest version. + /// The SMAPI version installed by the player. If this is null, the API won't provide a recommended update. + /// The Stardew Valley version installed by the player. + /// The OS on which the player plays. /// Whether to include extended metadata for each mod. - public IDictionary GetModInfo(ModSearchEntryModel[] mods, bool includeExtendedMetadata = false) + public IDictionary GetModInfo(ModSearchEntryModel[] mods, ISemanticVersion apiVersion, ISemanticVersion gameVersion, Platform platform, bool includeExtendedMetadata = false) { return this.Post( $"v{this.Version}/mods", - new ModSearchModel(mods, includeExtendedMetadata) + new ModSearchModel(mods, apiVersion, gameVersion, platform, includeExtendedMetadata) ).ToDictionary(p => p.ID); } diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs index 610e14f1..384f23fc 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs @@ -102,6 +102,8 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki string anchor = this.GetAttribute(node, "id"); string contentPackFor = this.GetAttribute(node, "data-content-pack-for"); string devNote = this.GetAttribute(node, "data-dev-note"); + IDictionary mapLocalVersions = this.GetAttributeAsVersionMapping(node, "data-map-local-versions"); + IDictionary mapRemoteVersions = this.GetAttributeAsVersionMapping(node, "data-map-remote-versions"); // parse stable compatibility WikiCompatibilityInfo compatibility = new WikiCompatibilityInfo @@ -159,6 +161,8 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki Warnings = warnings, MetadataLinks = metadataLinks.ToArray(), DevNote = devNote, + MapLocalVersions = mapLocalVersions, + MapRemoteVersions = mapRemoteVersions, Anchor = anchor }; } @@ -223,6 +227,28 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki return null; } + /// Get an attribute value and parse it as a version mapping. + /// The element whose attributes to read. + /// The attribute name. + private IDictionary GetAttributeAsVersionMapping(HtmlNode element, string name) + { + // get raw value + string raw = this.GetAttribute(element, name); + if (raw?.Contains("→") != true) + return null; + + // parse + // Specified on the wiki in the form "remote version → mapped version; another remote version → mapped version" + IDictionary map = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + foreach (string pair in raw.Split(';')) + { + string[] versions = pair.Split('→'); + if (versions.Length == 2 && !string.IsNullOrWhiteSpace(versions[0]) && !string.IsNullOrWhiteSpace(versions[1])) + map[versions[0].Trim()] = versions[1].Trim(); + } + return map; + } + /// Get the text of an element with the given class name. /// The metadata container. /// The field name. diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs index 51bb2336..931dcd43 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki { @@ -62,6 +63,12 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki /// Special notes intended for developers who maintain unofficial updates or submit pull requests. public string DevNote { get; set; } + /// Maps local versions to a semantic version for update checks. + public IDictionary MapLocalVersions { get; set; } + + /// Maps remote versions to a semantic version for update checks. + public IDictionary MapRemoteVersions { get; set; } + /// The link anchor for the mod entry in the wiki compatibility list. public string Anchor { get; set; } } diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDataModel.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDataModel.cs index dd0bd07b..8b40c301 100644 --- a/src/SMAPI.Toolkit/Framework/ModData/ModDataModel.cs +++ b/src/SMAPI.Toolkit/Framework/ModData/ModDataModel.cs @@ -25,12 +25,6 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData ///
public string FormerIDs { get; set; } - /// Maps local versions to a semantic version for update checks. - public IDictionary MapLocalVersions { get; set; } = new Dictionary(); - - /// Maps remote versions to a semantic version for update checks. - public IDictionary MapRemoteVersions { get; set; } = new Dictionary(); - /// The mod warnings to suppress, even if they'd normally be shown. public ModWarning SuppressWarnings { get; set; } diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs index f01ada7c..c892d820 100644 --- a/src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs +++ b/src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs @@ -22,12 +22,6 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData /// The mod warnings to suppress, even if they'd normally be shown. public ModWarning SuppressWarnings { get; set; } - /// Maps local versions to a semantic version for update checks. - public IDictionary MapLocalVersions { get; } - - /// Maps remote versions to a semantic version for update checks. - public IDictionary MapRemoteVersions { get; } - /// The versioned field data. public ModDataField[] Fields { get; } @@ -44,8 +38,6 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData this.ID = model.ID; this.FormerIDs = model.GetFormerIDs().ToArray(); this.SuppressWarnings = model.SuppressWarnings; - this.MapLocalVersions = new Dictionary(model.MapLocalVersions, StringComparer.InvariantCultureIgnoreCase); - this.MapRemoteVersions = new Dictionary(model.MapRemoteVersions, StringComparer.InvariantCultureIgnoreCase); this.Fields = model.GetFields().ToArray(); } @@ -67,29 +59,6 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData return false; } - /// Get a semantic local version for update checks. - /// The remote version to normalize. - public ISemanticVersion GetLocalVersionForUpdateChecks(ISemanticVersion version) - { - return this.MapLocalVersions != null && this.MapLocalVersions.TryGetValue(version.ToString(), out string newVersion) - ? new SemanticVersion(newVersion) - : version; - } - - /// Get a semantic remote version for update checks. - /// The remote version to normalize. - public string GetRemoteVersionForUpdateChecks(string version) - { - // normalize version if possible - if (SemanticVersion.TryParse(version, out ISemanticVersion parsed)) - version = parsed.ToString(); - - // fetch remote version - return this.MapRemoteVersions != null && this.MapRemoteVersions.TryGetValue(version, out string newVersion) - ? newVersion - : version; - } - /// Get the possible mod IDs. public IEnumerable GetIDs() { diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs index 9e22990d..598da66a 100644 --- a/src/SMAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs +++ b/src/SMAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs @@ -26,29 +26,5 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData /// The upper version for which the applies (if any). public ISemanticVersion StatusUpperVersion { get; set; } - - - /********* - ** Public methods - *********/ - /// Get a semantic local version for update checks. - /// The remote version to normalize. - public ISemanticVersion GetLocalVersionForUpdateChecks(ISemanticVersion version) - { - return this.DataRecord.GetLocalVersionForUpdateChecks(version); - } - - /// Get a semantic remote version for update checks. - /// The remote version to normalize. - public ISemanticVersion GetRemoteVersionForUpdateChecks(ISemanticVersion version) - { - if (version == null) - return null; - - string rawVersion = this.DataRecord.GetRemoteVersionForUpdateChecks(version.ToString()); - return rawVersion != null - ? new SemanticVersion(rawVersion) - : version; - } } } diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index f65b164f..fe220eb5 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -80,7 +80,7 @@ namespace StardewModdingAPI.Web.Controllers new IModRepository[] { new ChucklefishRepository(chucklefish), - new CurseForgeRepository(curseForge), + new CurseForgeRepository(curseForge), new GitHubRepository(github), new ModDropRepository(modDrop), new NexusRepository(nexus) @@ -90,12 +90,15 @@ namespace StardewModdingAPI.Web.Controllers /// Fetch version metadata for the given mods. /// The mod search criteria. + /// The requested API version. [HttpPost] - public async Task> PostAsync([FromBody] ModSearchModel model) + public async Task> PostAsync([FromBody] ModSearchModel model, [FromRoute] string version) { if (model?.Mods == null) return new ModEntryModel[0]; + bool legacyMode = SemanticVersion.TryParse(version, out ISemanticVersion parsedVersion) && parsedVersion.IsOlderThan("3.0.0-beta.20191109"); + // fetch wiki data WikiModEntry[] wikiData = this.WikiCache.GetWikiMods().Select(p => p.GetModel()).ToArray(); IDictionary mods = new Dictionary(StringComparer.CurrentCultureIgnoreCase); @@ -104,7 +107,25 @@ namespace StardewModdingAPI.Web.Controllers if (string.IsNullOrWhiteSpace(mod.ID)) continue; - ModEntryModel result = await this.GetModData(mod, wikiData, model.IncludeExtendedMetadata); + ModEntryModel result = await this.GetModData(mod, wikiData, model.IncludeExtendedMetadata || legacyMode, model.ApiVersion); + if (legacyMode) + { + result.Main = result.Metadata.Main; + result.Optional = result.Metadata.Optional; + result.Unofficial = result.Metadata.Unofficial; + result.UnofficialForBeta = result.Metadata.UnofficialForBeta; + result.HasBetaInfo = result.Metadata.BetaCompatibilityStatus != null; + result.SuggestedUpdate = null; + if (!model.IncludeExtendedMetadata) + result.Metadata = null; + } + else if (!model.IncludeExtendedMetadata && (model.ApiVersion == null || mod.InstalledVersion == null)) + { + var errors = new List(result.Errors); + errors.Add($"This API can't suggest an update because {nameof(model.ApiVersion)} or {nameof(mod.InstalledVersion)} are null, and you didn't specify {nameof(model.IncludeExtendedMetadata)} to get other info. See the SMAPI technical docs for usage."); + result.Errors = errors.ToArray(); + } + mods[mod.ID] = result; } @@ -120,8 +141,9 @@ namespace StardewModdingAPI.Web.Controllers /// The mod data to match. /// The wiki data. /// Whether to include extended metadata for each mod. + /// The SMAPI version installed by the player. /// Returns the mod data if found, else null. - private async Task GetModData(ModSearchEntryModel search, WikiModEntry[] wikiData, bool includeExtendedMetadata) + private async Task GetModData(ModSearchEntryModel search, WikiModEntry[] wikiData, bool includeExtendedMetadata, ISemanticVersion apiVersion) { // cross-reference data ModDataRecord record = this.ModDatabase.Get(search.ID); @@ -131,6 +153,10 @@ namespace StardewModdingAPI.Web.Controllers // get latest versions ModEntryModel result = new ModEntryModel { ID = search.ID }; IList errors = new List(); + ModEntryVersionModel main = null; + ModEntryVersionModel optional = null; + ModEntryVersionModel unofficial = null; + ModEntryVersionModel unofficialForBeta = null; foreach (UpdateKey updateKey in updateKeys) { // validate update key @@ -151,76 +177,118 @@ namespace StardewModdingAPI.Web.Controllers // handle main version if (data.Version != null) { - if (!SemanticVersion.TryParse(data.Version, out ISemanticVersion version)) + ISemanticVersion version = this.GetMappedVersion(data.Version, wikiEntry?.MapRemoteVersions); + if (version == null) { errors.Add($"The update key '{updateKey}' matches a mod with invalid semantic version '{data.Version}'."); continue; } - if (this.IsNewer(version, result.Main?.Version)) - result.Main = new ModEntryVersionModel(version, data.Url); + if (this.IsNewer(version, main?.Version)) + main = new ModEntryVersionModel(version, data.Url); } // handle optional version if (data.PreviewVersion != null) { - if (!SemanticVersion.TryParse(data.PreviewVersion, out ISemanticVersion version)) + ISemanticVersion version = this.GetMappedVersion(data.PreviewVersion, wikiEntry?.MapRemoteVersions); + if (version == null) { errors.Add($"The update key '{updateKey}' matches a mod with invalid optional semantic version '{data.PreviewVersion}'."); continue; } - if (this.IsNewer(version, result.Optional?.Version)) - result.Optional = new ModEntryVersionModel(version, data.Url); + if (this.IsNewer(version, optional?.Version)) + optional = new ModEntryVersionModel(version, data.Url); } } // get unofficial version - if (wikiEntry?.Compatibility.UnofficialVersion != null && this.IsNewer(wikiEntry.Compatibility.UnofficialVersion, result.Main?.Version) && this.IsNewer(wikiEntry.Compatibility.UnofficialVersion, result.Optional?.Version)) - result.Unofficial = new ModEntryVersionModel(wikiEntry.Compatibility.UnofficialVersion, $"{this.CompatibilityPageUrl}/#{wikiEntry.Anchor}"); + if (wikiEntry?.Compatibility.UnofficialVersion != null && this.IsNewer(wikiEntry.Compatibility.UnofficialVersion, main?.Version) && this.IsNewer(wikiEntry.Compatibility.UnofficialVersion, optional?.Version)) + unofficial = new ModEntryVersionModel(wikiEntry.Compatibility.UnofficialVersion, $"{this.CompatibilityPageUrl}/#{wikiEntry.Anchor}"); // get unofficial version for beta if (wikiEntry?.HasBetaInfo == true) { - result.HasBetaInfo = true; if (wikiEntry.BetaCompatibility.Status == WikiCompatibilityStatus.Unofficial) { if (wikiEntry.BetaCompatibility.UnofficialVersion != null) { - result.UnofficialForBeta = (wikiEntry.BetaCompatibility.UnofficialVersion != null && this.IsNewer(wikiEntry.BetaCompatibility.UnofficialVersion, result.Main?.Version) && this.IsNewer(wikiEntry.BetaCompatibility.UnofficialVersion, result.Optional?.Version)) + unofficialForBeta = (wikiEntry.BetaCompatibility.UnofficialVersion != null && this.IsNewer(wikiEntry.BetaCompatibility.UnofficialVersion, main?.Version) && this.IsNewer(wikiEntry.BetaCompatibility.UnofficialVersion, optional?.Version)) ? new ModEntryVersionModel(wikiEntry.BetaCompatibility.UnofficialVersion, $"{this.CompatibilityPageUrl}/#{wikiEntry.Anchor}") : null; } else - result.UnofficialForBeta = result.Unofficial; + unofficialForBeta = unofficial; } } // fallback to preview if latest is invalid - if (result.Main == null && result.Optional != null) + if (main == null && optional != null) { - result.Main = result.Optional; - result.Optional = null; + main = optional; + optional = null; } // special cases if (result.ID == "Pathoschild.SMAPI") { - if (result.Main != null) - result.Main.Url = "https://smapi.io/"; - if (result.Optional != null) - result.Optional.Url = "https://smapi.io/"; + if (main != null) + main.Url = "https://smapi.io/"; + if (optional != null) + optional.Url = "https://smapi.io/"; + } + + // get recommended update (if any) + ISemanticVersion installedVersion = this.GetMappedVersion(search.InstalledVersion?.ToString(), wikiEntry?.MapLocalVersions); + if (apiVersion != null && installedVersion != null) + { + // get newer versions + List updates = new List(); + if (this.IsRecommendedUpdate(installedVersion, main?.Version, useBetaChannel: true)) + updates.Add(main); + if (this.IsRecommendedUpdate(installedVersion, optional?.Version, useBetaChannel: installedVersion.IsPrerelease())) + updates.Add(optional); + if (this.IsRecommendedUpdate(installedVersion, unofficial?.Version, useBetaChannel: search.IsBroken)) + updates.Add(unofficial); + if (this.IsRecommendedUpdate(installedVersion, unofficialForBeta?.Version, useBetaChannel: apiVersion.IsPrerelease())) + updates.Add(unofficialForBeta); + + // get newest version + ModEntryVersionModel newest = null; + foreach (ModEntryVersionModel update in updates) + { + if (newest == null || update.Version.IsNewerThan(newest.Version)) + newest = update; + } + + // set field + result.SuggestedUpdate = newest != null + ? new ModEntryVersionModel(newest.Version, newest.Url) + : null; } // add extended metadata - if (includeExtendedMetadata && (wikiEntry != null || record != null)) - result.Metadata = new ModExtendedMetadataModel(wikiEntry, record); + if (includeExtendedMetadata) + result.Metadata = new ModExtendedMetadataModel(wikiEntry, record, main: main, optional: optional, unofficial: unofficial, unofficialForBeta: unofficialForBeta); // add result result.Errors = errors.ToArray(); return result; } + /// Get whether a given version should be offered to the user as an update. + /// The current semantic version. + /// The target semantic version. + /// Whether the user enabled the beta channel and should be offered prerelease updates. + private bool IsRecommendedUpdate(ISemanticVersion currentVersion, ISemanticVersion newVersion, bool useBetaChannel) + { + return + newVersion != null + && newVersion.IsNewerThan(currentVersion) + && (useBetaChannel || !newVersion.IsPrerelease()); + } + /// Get whether a version is newer than an version. /// The current version. /// The other version. @@ -260,7 +328,7 @@ namespace StardewModdingAPI.Web.Controllers /// The specified update keys. /// The mod's entry in SMAPI's internal database. /// The mod's entry in the wiki list. - public IEnumerable GetUpdateKeys(string[] specifiedKeys, ModDataRecord record, WikiModEntry entry) + private IEnumerable GetUpdateKeys(string[] specifiedKeys, ModDataRecord record, WikiModEntry entry) { IEnumerable GetRaw() { @@ -301,5 +369,49 @@ namespace StardewModdingAPI.Web.Controllers yield return key; } } + + /// Get a semantic local version for update checks. + /// The version to parse. + /// A map of version replacements. + private ISemanticVersion GetMappedVersion(string version, IDictionary map) + { + // try mapped version + string rawNewVersion = this.GetRawMappedVersion(version, map); + if (SemanticVersion.TryParse(rawNewVersion, out ISemanticVersion parsedNew)) + return parsedNew; + + // return original version + return SemanticVersion.TryParse(version, out ISemanticVersion parsedOld) + ? parsedOld + : null; + } + + /// Get a semantic local version for update checks. + /// The version to map. + /// A map of version replacements. + private string GetRawMappedVersion(string version, IDictionary map) + { + if (version == null || map == null || !map.Any()) + return version; + + // match exact raw version + if (map.ContainsKey(version)) + return map[version]; + + // match parsed version + if (SemanticVersion.TryParse(version, out ISemanticVersion parsed)) + { + if (map.ContainsKey(parsed.ToString())) + return map[parsed.ToString()]; + + foreach (var pair in map) + { + if (SemanticVersion.TryParse(pair.Key, out ISemanticVersion target) && parsed.Equals(target) && SemanticVersion.TryParse(pair.Value, out ISemanticVersion newVersion)) + return newVersion.ToString(); + } + } + + return version; + } } } diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs b/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs index fddf99ee..8569984a 100644 --- a/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs +++ b/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs @@ -1,6 +1,9 @@ using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Bson.Serialization.Options; using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; @@ -109,6 +112,17 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki /// The URL to the latest unofficial update, if applicable. public string BetaUnofficialUrl { get; set; } + /**** + ** Version maps + ****/ + /// Maps local versions to a semantic version for update checks. + [BsonDictionaryOptions(Representation = DictionaryRepresentation.ArrayOfArrays)] + public IDictionary MapLocalVersions { get; set; } + + /// Maps remote versions to a semantic version for update checks. + [BsonDictionaryOptions(Representation = DictionaryRepresentation.ArrayOfArrays)] + public IDictionary MapRemoteVersions { get; set; } + /********* ** Accessors @@ -154,6 +168,10 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki this.BetaBrokeIn = mod.BetaCompatibility?.BrokeIn; this.BetaUnofficialVersion = mod.BetaCompatibility?.UnofficialVersion?.ToString(); this.BetaUnofficialUrl = mod.BetaCompatibility?.UnofficialUrl; + + // version maps + this.MapLocalVersions = mod.MapLocalVersions; + this.MapRemoteVersions = mod.MapRemoteVersions; } /// Reconstruct the original model. @@ -186,7 +204,11 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki BrokeIn = this.MainBrokeIn, UnofficialVersion = this.MainUnofficialVersion != null ? new SemanticVersion(this.MainUnofficialVersion) : null, UnofficialUrl = this.MainUnofficialUrl - } + }, + + // version maps + MapLocalVersions = this.MapLocalVersions, + MapRemoteVersions = this.MapRemoteVersions }; // beta compatibility diff --git a/src/SMAPI.Web/Views/Index/Privacy.cshtml b/src/SMAPI.Web/Views/Index/Privacy.cshtml index 356580b4..914384a8 100644 --- a/src/SMAPI.Web/Views/Index/Privacy.cshtml +++ b/src/SMAPI.Web/Views/Index/Privacy.cshtml @@ -24,7 +24,7 @@

This website and SMAPI's web API are hosted by Amazon Web Services. Their servers may automatically collect diagnostics like your IP address, but this information is not visible to SMAPI's web application or developers. For more information, see the Amazon Privacy Notice.

Update checks

-

SMAPI notifies you when there's a new version of SMAPI or your mods available. To do so, it sends your SMAPI and mod versions to its web API. No personal information is stored by the web application, but see web logging.

+

SMAPI notifies you when there's a new version of SMAPI or your mods available. To do so, it sends your game/SMAPI/mod versions and platform type to its web API. No personal information is stored by the web application, but see web logging.

You can disable update checks, and no information will be transmitted to the web API. To do so:

    diff --git a/src/SMAPI.Web/wwwroot/SMAPI.metadata.json b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json index e4c0a6f6..553b6934 100644 --- a/src/SMAPI.Web/wwwroot/SMAPI.metadata.json +++ b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json @@ -14,11 +14,6 @@ * other fields if no ID was specified. This doesn't include the latest ID, if any. Multiple * variants can be separated with '|'. * - * - MapLocalVersions and MapRemoteVersions correct local manifest versions and remote versions - * during update checks. For example, if the API returns version '1.1-1078' where '1078' is - * intended to be a build number, MapRemoteVersions can map it to '1.1' when comparing to the - * mod's current version. This is only meant to support legacy mods with injected update keys. - * * Versioned metadata * ================== * Each record can also specify extra metadata using the field keys below. @@ -122,91 +117,6 @@ "Default | UpdateKey": "Nexus:1820" }, - - /********* - ** Map versions - *********/ - "Adjust Artisan Prices": { - "ID": "ThatNorthernMonkey.AdjustArtisanPrices", - "FormerIDs": "1e36d4ca-c7ef-4dfb-9927-d27a6c3c8bdc", // changed in 0.0.2-pathoschild-update - "MapRemoteVersions": { "0.01": "0.0.1" } - }, - - "Almighty Farming Tool": { - "ID": "439", - "MapRemoteVersions": { - "1.21": "1.2.1", - "1.22-unofficial.3.mizzion": "1.2.2-unofficial.3.mizzion" - } - }, - - "Basic Sprinkler Improved": { - "ID": "lrsk_sdvm_bsi.0117171308", - "MapRemoteVersions": { "1.0.2": "1.0.1-release" } // manifest not updated - }, - - "Better Shipping Box": { - "ID": "Kithio:BetterShippingBox", - "MapLocalVersions": { "1.0.1": "1.0.2" } - }, - - "Chefs Closet": { - "ID": "Duder.ChefsCloset", - "MapLocalVersions": { "1.3-1": "1.3" } - }, - - "Configurable Machines": { - "ID": "21da6619-dc03-4660-9794-8e5b498f5b97", - "MapLocalVersions": { "1.2-beta": "1.2" } - }, - - "Crafting Counter": { - "ID": "lolpcgaming.CraftingCounter", - "MapRemoteVersions": { "1.1": "1.0" } // not updated in manifest - }, - - "Custom Linens": { - "ID": "Mevima.CustomLinens", - "MapRemoteVersions": { "1.1": "1.0" } // manifest not updated - }, - - "Dynamic Horses": { - "ID": "Bpendragon-DynamicHorses", - "MapRemoteVersions": { "1.2": "1.1-release" } // manifest not updated - }, - - "Dynamic Machines": { - "ID": "DynamicMachines", - "MapLocalVersions": { "1.1": "1.1.1" } - }, - - "Multiple Sprites and Portraits On Rotation (File Loading)": { - "ID": "FileLoading", - "MapLocalVersions": { "1.1": "1.12" } - }, - - "Relationship Status": { - "ID": "relationshipstatus", - "MapRemoteVersions": { "1.0.5": "1.0.4" } // not updated in manifest - }, - - "ReRegeneration": { - "ID": "lrsk_sdvm_rerg.0925160827", - "MapLocalVersions": { "1.1.2-release": "1.1.2" } - }, - - "Showcase Mod": { - "ID": "Igorious.Showcase", - "MapLocalVersions": { "0.9-500": "0.9" } - }, - - "Siv's Marriage Mod": { - "ID": "6266959802", // official version - "FormerIDs": "Siv.MarriageMod | medoli900.Siv's Marriage Mod", // 1.2.3-unofficial versions - "MapLocalVersions": { "0.0": "1.4" } - }, - - /********* ** Obsolete *********/ @@ -477,12 +387,6 @@ "~1.0.0 | Status": "AssumeBroken" // broke in Stardew Valley 1.3.29 (runtime errors) }, - "Skill Prestige: Cooking Adapter": { - "ID": "Alphablackwolf.CookingSkillPrestigeAdapter", - "FormerIDs": "20d6b8a3-b6e7-460b-a6e4-07c2b0cb6c63", // changed circa 1.1 - "MapRemoteVersions": { "1.2.3": "1.1" } // manifest not updated - }, - "Skull Cave Saver": { "ID": "cantorsdust.SkullCaveSaver", "FormerIDs": "8ac06349-26f7-4394-806c-95d48fd35774 | community.SkullCaveSaver", // changed in 1.1 and 1.2.2 @@ -501,7 +405,6 @@ "Stephan's Lots of Crops": { "ID": "stephansstardewcrops", - "MapRemoteVersions": { "1.41": "1.1" }, // manifest not updated "~1.1 | Status": "AssumeBroken" // broke in SDV 1.3 (overwrites vanilla items) }, diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 50bd562a..77b17c8a 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -593,27 +593,19 @@ namespace StardewModdingAPI.Framework ISemanticVersion updateFound = null; try { - ModEntryModel response = client.GetModInfo(new[] { new ModSearchEntryModel("Pathoschild.SMAPI", new[] { $"GitHub:{this.Settings.GitHubProjectName}" }) }).Single().Value; - ISemanticVersion latestStable = response.Main?.Version; - ISemanticVersion latestBeta = response.Optional?.Version; + // fetch update check + ModEntryModel response = client.GetModInfo(new[] { new ModSearchEntryModel("Pathoschild.SMAPI", Constants.ApiVersion, new[] { $"GitHub:{this.Settings.GitHubProjectName}" }) }, apiVersion: Constants.ApiVersion, gameVersion: Constants.GameVersion, platform: Constants.Platform).Single().Value; + if (response.SuggestedUpdate != null) + this.Monitor.Log($"You can update SMAPI to {response.SuggestedUpdate.Version}: {Constants.HomePageUrl}", LogLevel.Alert); + else + this.Monitor.Log(" SMAPI okay.", LogLevel.Trace); - if (latestStable == null && response.Errors.Any()) + // show errors + if (response.Errors.Any()) { 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: {string.Join("\n", response.Errors)}", LogLevel.Trace); } - else if (this.IsValidUpdate(Constants.ApiVersion, latestBeta, this.Settings.UseBetaChannel)) - { - updateFound = latestBeta; - this.Monitor.Log($"You can update SMAPI to {latestBeta}: {Constants.HomePageUrl}", LogLevel.Alert); - } - else if (this.IsValidUpdate(Constants.ApiVersion, latestStable, this.Settings.UseBetaChannel)) - { - updateFound = latestStable; - this.Monitor.Log($"You can update SMAPI to {latestStable}: {Constants.HomePageUrl}", LogLevel.Alert); - } - else - this.Monitor.Log(" SMAPI okay.", LogLevel.Trace); } catch (Exception ex) { @@ -646,12 +638,12 @@ namespace StardewModdingAPI.Framework .GetUpdateKeys(validOnly: true) .Select(p => p.ToString()) .ToArray(); - searchMods.Add(new ModSearchEntryModel(mod.Manifest.UniqueID, updateKeys.ToArray())); + searchMods.Add(new ModSearchEntryModel(mod.Manifest.UniqueID, mod.Manifest.Version, updateKeys.ToArray(), isBroken: mod.Status == ModMetadataStatus.Failed)); } // fetch results this.Monitor.Log($" Checking for updates to {searchMods.Count} mods...", LogLevel.Trace); - IDictionary results = client.GetModInfo(searchMods.ToArray()); + IDictionary results = client.GetModInfo(searchMods.ToArray(), apiVersion: Constants.ApiVersion, gameVersion: Constants.GameVersion, platform: Constants.Platform); // extract update alerts & errors var updates = new List>(); @@ -672,20 +664,9 @@ namespace StardewModdingAPI.Framework ); } - // parse versions - bool useBetaInfo = result.HasBetaInfo && Constants.ApiVersion.IsPrerelease(); - ISemanticVersion localVersion = mod.DataRecord?.GetLocalVersionForUpdateChecks(mod.Manifest.Version) ?? mod.Manifest.Version; - ISemanticVersion latestVersion = mod.DataRecord?.GetRemoteVersionForUpdateChecks(result.Main?.Version) ?? result.Main?.Version; - ISemanticVersion optionalVersion = mod.DataRecord?.GetRemoteVersionForUpdateChecks(result.Optional?.Version) ?? result.Optional?.Version; - ISemanticVersion unofficialVersion = useBetaInfo ? result.UnofficialForBeta?.Version : result.Unofficial?.Version; - - // show update alerts - if (this.IsValidUpdate(localVersion, latestVersion, useBetaChannel: true)) - updates.Add(Tuple.Create(mod, latestVersion, result.Main?.Url)); - else if (this.IsValidUpdate(localVersion, optionalVersion, useBetaChannel: localVersion.IsPrerelease())) - updates.Add(Tuple.Create(mod, optionalVersion, result.Optional?.Url)); - else if (this.IsValidUpdate(localVersion, unofficialVersion, useBetaChannel: mod.Status == ModMetadataStatus.Failed)) - updates.Add(Tuple.Create(mod, unofficialVersion, useBetaInfo ? result.UnofficialForBeta?.Url : result.Unofficial?.Url)); + // handle update + if (result.SuggestedUpdate != null) + updates.Add(Tuple.Create(mod, result.SuggestedUpdate.Version, result.SuggestedUpdate.Url)); } // show update errors @@ -720,18 +701,6 @@ namespace StardewModdingAPI.Framework }).Start(); } - /// Get whether a given version should be offered to the user as an update. - /// The current semantic version. - /// The target semantic version. - /// Whether the user enabled the beta channel and should be offered prerelease updates. - private bool IsValidUpdate(ISemanticVersion currentVersion, ISemanticVersion newVersion, bool useBetaChannel) - { - return - newVersion != null - && newVersion.IsNewerThan(currentVersion) - && (useBetaChannel || !newVersion.IsPrerelease()); - } - /// Create a directory path if it doesn't exist. /// The directory path. private void VerifyPath(string path) -- cgit