From 1dde811c36c2d03c17df4d09d978907bbd608787 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 6 Jul 2019 01:04:05 -0400 Subject: group technical docs, add general shortcut for mod build package docs (#651) --- docs/technical/mod-package.md | 371 +++++++++++++++++++++ .../screenshots/code-analyzer-example.png | Bin 0 -> 3473 bytes docs/technical/smapi.md | 105 ++++++ docs/technical/web.md | 106 ++++++ 4 files changed, 582 insertions(+) create mode 100644 docs/technical/mod-package.md create mode 100644 docs/technical/screenshots/code-analyzer-example.png create mode 100644 docs/technical/smapi.md create mode 100644 docs/technical/web.md (limited to 'docs/technical') diff --git a/docs/technical/mod-package.md b/docs/technical/mod-package.md new file mode 100644 index 00000000..43682bfb --- /dev/null +++ b/docs/technical/mod-package.md @@ -0,0 +1,371 @@ +← [SMAPI](../README.md) + +The **mod build package** is an open-source NuGet package which automates the MSBuild configuration +for SMAPI mods and related tools. The package is fully compatible with Linux, Mac, and Windows. + +## Contents +* [Use](#use) +* [Features](#features) + * [Detect game path](#detect-game-path) + * [Add assembly references](#add-assembly-references) + * [Copy files into the `Mods` folder and create release zip](#copy-files-into-the-mods-folder-and-create-release-zip) + * [Launch or debug game](#launch-or-debug-game) + * [Preconfigure common settings](#preconfigure-common-settings) + * [Add code warnings](#add-code-warnings) +* [Code warnings](#code-warnings) +* [Special cases](#special-cases) + * [Custom game path](#custom-game-path) + * [Non-mod projects](#non-mod-projects) +* [For SMAPI developers](#for-smapi-developers) +* [Release notes](#release-notes) + +## Use +1. Create an empty library project. +2. Reference the [`Pathoschild.Stardew.ModBuildConfig` NuGet package](https://www.nuget.org/packages/Pathoschild.Stardew.ModBuildConfig). +3. [Write your code](https://stardewvalleywiki.com/Modding:Creating_a_SMAPI_mod). +4. Compile on any platform. +5. Run the game to play with your mod. + +## Features +The package automatically makes the changes listed below. In some cases you can configure how it +works by editing your mod's `.csproj` file, and adding the given properties between the first +`` and `` tags. + +### Detect game path +The package finds your game folder by scanning the default install paths and Windows registry. It +adds two MSBuild properties for use in your `.csproj` file if needed: + +property | description +-------- | ----------- +`$(GamePath)` | The absolute path to the detected game folder. +`$(GameExecutableName)` | The game's executable name for the current OS (`Stardew Valley` on Windows, or `StardewValley` on Linux/Mac). + +If you get a build error saying it can't find your game, see [_set the game path_](#set-the-game-path). + +### Add assembly references +The package adds assembly references to SMAPI, Stardew Valley, xTile, and MonoGame (Linux/Mac) or XNA +Framework (Windows). It automatically adjusts depending on which OS you're compiling it on. + +The assemblies aren't copied to the build output, since mods loaded by SMAPI won't need them. For +non-mod projects like unit tests, you can set this property: +```xml +true +``` + +If your mod uses [Harmony](https://github.com/pardeike/Harmony) (not recommended for most mods), +the package can add a reference to SMAPI's Harmony DLL for you: +```xml +true +``` + +### Copy files into the `Mods` folder and create release zip +
+
Files considered part of your mod
+
+ +These files are selected by default: `manifest.json`, +[`i18n` files](https://stardewvalleywiki.com/Modding:Translations) (if any), the `assets` folder +(if any), and all files in the build output. You can select custom files by [adding them to the +build output](https://stackoverflow.com/a/10828462/262123). (If your project references another mod, +make sure the reference is [_not_ marked 'copy local'](https://msdn.microsoft.com/en-us/library/t1zz5y8c(v=vs.100).aspx).) + +You can deselect a file by removing it from the build output. For a default file, you can set the +property below to a comma-delimited list of regex patterns to ignore. For crossplatform +compatibility, you should replace path delimiters with `[/\\]`. + +```xml +\.txt$, \.pdf$, assets[/\\]paths.png +``` + +
+
Copy files into the `Mods` folder
+
+ +The package copies the selected files into your game's `Mods` folder when you rebuild the code, +with a subfolder matching the mod's project name. + +You can change the folder name: +```xml +YourModName +``` + +Or disable deploying the files: +```xml +false +``` + +
+
Create release zip
+
+ +The package adds a zip file in your project's `bin` folder when you rebuild the code, in the format +recommended for sites like Nexus Mods. The zip filename can be changed using `ModFolderName` above. + +You can change the folder path where the zip is created: +```xml +$(SolutionDir)\_releases +``` + +Or disable zip creation: +```xml +false +``` + +
+
+ +### Launch or debug game +On Windows only, the package configures Visual Studio so you can launch the game and attach a +debugger using _Debug > Start Debugging_ or _Debug > Start Without Debugging_. This lets you [set +breakpoints](https://docs.microsoft.com/en-us/visualstudio/debugger/using-breakpoints?view=vs-2019) +in your code while the game is running, or [make simple changes to the mod code without needing to +restart the game](https://docs.microsoft.com/en-us/visualstudio/debugger/edit-and-continue?view=vs-2019). + +This is disabled on Linux/Mac due to limitations with the Mono wrapper. + +To disable game debugging (only needed for some non-mod projects): + +```xml +false +``` + +### Preconfigure common settings +The package automatically enables PDB files, so error logs show line numbers for simpler debugging. + +For projects using the simplified `.csproj` format, it also enables the GAC (to support XNA +Framework) and sets the build to x86 mode (to avoid 'mismatch between the processor architecture' warnings due to + the game being x86). + +### Add code warnings +The package runs code analysis on your mod and raises warnings for some common errors or pitfalls. +See [_code warnings_](#code-warnings) for more info. + +## Code warnings +### Overview +The NuGet package adds code warnings in Visual Studio specific to Stardew Valley. For example: +![](screenshots/code-analyzer-example.png) + +You can hide the warnings using the warning ID (shown under 'code' in the Error List). See... +* [for specific code](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/preprocessor-directives/preprocessor-pragma-warning); +* for a method using this attribute: + ```cs + [System.Diagnostics.CodeAnalysis.SuppressMessage("SMAPI.CommonErrors", "AvoidNetField")] + ``` +* for an entire project: + 1. Expand the _References_ node for the project in Visual Studio. + 2. Right-click on _Analyzers_ and choose _Open Active Rule Set_. + 4. Expand _StardewModdingAPI.ModBuildConfig.Analyzer_ and uncheck the warnings you want to hide. + +See below for help with each specific warning. + +### Avoid implicit net field cast +Warning text: +> This implicitly converts '{{expression}}' from {{net type}} to {{other type}}, but +> {{net type}} has unintuitive implicit conversion rules. Consider comparing against the actual +> value instead to avoid bugs. + +Stardew Valley uses net types (like `NetBool` and `NetInt`) to handle multiplayer sync. These types +can implicitly convert to their equivalent normal values (like `bool x = new NetBool()`), but their +conversion rules are unintuitive and error-prone. For example, +`item?.category == null && item?.category != null` can both be true at once, and +`building.indoors != null` can be true for a null value. + +Suggested fix: +* Some net fields have an equivalent non-net property like `monster.Health` (`int`) instead of + `monster.health` (`NetInt`). The package will add a separate [AvoidNetField](#avoid-net-field) warning for + these. Use the suggested property instead. +* For a reference type (i.e. one that can contain `null`), you can use the `.Value` property: + ```c# + if (building.indoors.Value == null) + ``` + Or convert the value before comparison: + ```c# + GameLocation indoors = building.indoors; + if(indoors == null) + // ... + ``` +* For a value type (i.e. one that can't contain `null`), check if the object is null (if applicable) + and compare with `.Value`: + ```cs + if (item != null && item.category.Value == 0) + ``` + +### Avoid net field +Warning text: +> '{{expression}}' is a {{net type}} field; consider using the {{property name}} property instead. + +Your code accesses a net field, which has some unusual behavior (see [AvoidImplicitNetFieldCast](#avoid-implicit-net-field-cast)). +This field has an equivalent non-net property that avoids those issues. + +Suggested fix: access the suggested property name instead. + +### Avoid obsolete field +Warning text: +> The '{{old field}}' field is obsolete and should be replaced with '{{new field}}'. + +Your code accesses a field which is obsolete or no longer works. Use the suggested field instead. + +## Special cases +### Custom game path +The package usually detects where your game is installed automatically. If it can't find your game +or you have multiple installs, you can specify the path yourself. There's two ways to do that: + +* **Option 1: global game path (recommended).** + _This will apply to every project that uses the package._ + + 1. Get the full folder path containing the Stardew Valley executable. + 2. Create this file: + + platform | path + --------- | ---- + Linux/Mac | `~/stardewvalley.targets` + Windows | `%USERPROFILE%\stardewvalley.targets` + + 3. Save the file with this content: + + ```xml + + + PATH_HERE + + + ``` + + 4. Replace `PATH_HERE` with your game path. + +* **Option 2: path in the project file.** + _You'll need to do this for each project that uses the package._ + + 1. Get the folder path containing the Stardew Valley `.exe` file. + 2. Add this to your `.csproj` file under the ` + PATH_HERE + + ``` + + 3. Replace `PATH_HERE` with your custom game install path. + +The configuration will check your custom path first, then fall back to the default paths (so it'll +still compile on a different computer). + +You access the game path via `$(GamePath)` in MSBuild properties, if you need to reference another +file in the game folder. + +### Non-mod projects +You can use the package in non-mod projects too (e.g. unit tests or framework DLLs). Just disable +the mod-related package features: + +```xml +false +false +false +``` + +If you need to copy the referenced DLLs into your build output, add this too: +```xml +true +``` + +## For SMAPI developers +The mod build package consists of three projects: + +project | purpose +------------------------------------------------- | ---------------- +`StardewModdingAPI.ModBuildConfig` | Configures the build (references, deploying the mod files, setting up debugging, etc). +`StardewModdingAPI.ModBuildConfig.Analyzer` | Adds C# analyzers which show code warnings in Visual Studio. +`StardewModdingAPI.ModBuildConfig.Analyzer.Tests` | Unit tests for the C# analyzers. + +To prepare a build of the NuGet package: +1. Install the [NuGet CLI](https://docs.microsoft.com/en-us/nuget/install-nuget-client-tools#nugetexe-cli). +1. Change the version and release notes in `package.nuspec`. +2. Rebuild the solution in _Release_ mode. +3. Open a terminal in the `bin/Pathoschild.Stardew.ModBuildConfig` package and run this command: + ```bash + nuget.exe pack + ``` + +That will create a `Pathoschild.Stardew.ModBuildConfig-.nupkg` file in the same directory +which can be uploaded to NuGet or referenced directly. + +## Release notes +### Upcoming release +* Updated for SMAPI 3.0 and Stardew Valley 1.4. +* Added automatic support for `assets` folders. +* Added `$(GameExecutableName)` MSBuild variable. +* Added support for projects using the simplified `.csproj` format: + * platform target is now set to x86 automatically to avoid mismatching platform target warnings; + * added GAC to assembly search paths to fix references to XNA Framework. +* Added option to disable game debugging config. +* Added `.pdb` files to builds by default (to enable line numbers in error stack traces). +* Added optional Harmony reference. +* Fixed `Newtonsoft.Json.pdb` included in release zips when Json.NET is referenced directly. +* Fixed `` not working for `i18n` files. +* Dropped support for older versions of SMAPI and Visual Studio. + +### 2.2 +* Added support for SMAPI 2.8+ (still compatible with earlier versions). +* Added default game paths for 32-bit Windows. +* Fixed valid manifests marked invalid in some cases. + +### 2.1 +* Added support for Stardew Valley 1.3. +* Added support for non-mod projects. +* Added C# analyzers to warn about implicit conversions of Netcode fields in Stardew Valley 1.3. +* Added option to ignore files by regex pattern. +* Added reference to new SMAPI DLL. +* Fixed some game paths not detected by NuGet package. + +### 2.0.2 +* Fixed compatibility issue on Linux. + +### 2.0.1 +* Fixed mod deploy failing to create subfolders if they don't already exist. + +### 2.0 +* Added: mods are now copied into the `Mods` folder automatically (configurable). +* Added: release zips are now created automatically in your build output folder (configurable). +* Added: mod deploy and release zips now exclude Json.NET automatically, since it's provided by SMAPI. +* Added mod's version to release zip filename. +* Improved errors to simplify troubleshooting. +* Fixed release zip not having a mod folder. +* Fixed release zip failing if mod name contains characters that aren't valid in a filename. + +### 1.7.1 +* Fixed issue where i18n folders were flattened. +* The manifest/i18n files in the project now take precedence over those in the build output if both + are present. + +### 1.7 +* Added option to create release zips on build. +* Added reference to XNA's XACT library for audio-related mods. + +### 1.6 +* Added support for deploying mod files into `Mods` automatically. +* Added a build error if a game folder is found, but doesn't contain Stardew Valley or SMAPI. + +### 1.5 +* Added support for setting a custom game path globally. +* Added default GOG path on Mac. + +### 1.4 +* Fixed detection of non-default game paths on 32-bit Windows. +* Removed support for SilVerPLuM (discontinued). +* Removed support for overriding the target platform (no longer needed since SMAPI crossplatforms + mods automatically). + +### 1.3 +* Added support for non-default game paths on Windows. + +### 1.2 +* Exclude game binaries from mod build output. + +### 1.1 +* Added support for overriding the target platform. + +### 1.0 +* Initial release. +* Added support for detecting the game path automatically. +* Added support for injecting XNA/MonoGame references automatically based on the OS. +* Added support for mod builders like SilVerPLuM. diff --git a/docs/technical/screenshots/code-analyzer-example.png b/docs/technical/screenshots/code-analyzer-example.png new file mode 100644 index 00000000..de38f643 Binary files /dev/null and b/docs/technical/screenshots/code-analyzer-example.png differ diff --git a/docs/technical/smapi.md b/docs/technical/smapi.md new file mode 100644 index 00000000..a006ff1a --- /dev/null +++ b/docs/technical/smapi.md @@ -0,0 +1,105 @@ +← [README](../README.md) + +This file provides more technical documentation about SMAPI. If you only want to use or create +mods, this section isn't relevant to you; see the main README to use or create mods. + +This document is about SMAPI itself; see also [mod build package](mod-package.md) and +[web services](web.md). + +# Contents +* [Customisation](#customisation) + * [Configuration file](#configuration-file) + * [Command-line arguments](#command-line-arguments) + * [Compile flags](#compile-flags) +* [For SMAPI developers](#for-smapi-developers) + * [Compiling from source](#compiling-from-source) + * [Debugging a local build](#debugging-a-local-build) + * [Preparing a release](#preparing-a-release) +* [Release notes](#release-notes) + +## Customisation +### Configuration file +You can customise the SMAPI behaviour by editing the `smapi-internal/config.json` file in your game +folder. + +Basic fields: + +field | purpose +----------------- | ------- +`DeveloperMode` | Default `false` (except in _SMAPI for developers_ releases). Whether to enable features intended for mod developers (mainly more detailed console logging). +`CheckForUpdates` | Default `true`. Whether SMAPI should check for a newer version when you load the game. If a new version is available, a small message will appear in the console. This doesn't affect the load time even if your connection is offline or slow, because it happens in the background. +`VerboseLogging` | Default `false`. Whether SMAPI should log more information about the game context. +`ModData` | Internal metadata about SMAPI mods. Changing this isn't recommended and may destabilise your game. See documentation in the file. + +### Command-line arguments +The SMAPI installer recognises three command-line arguments: + +argument | purpose +-------- | ------- +`--install` | Preselects the install action, skipping the prompt asking what the user wants to do. +`--uninstall` | Preselects the uninstall action, skipping the prompt asking what the user wants to do. +`--game-path "path"` | Specifies the full path to the folder containing the Stardew Valley executable, skipping automatic detection and any prompt to choose a path. If the path is not valid, the installer displays an error. + +SMAPI itself recognises two arguments, but these are intended for internal use or testing and may +change without warning. + +argument | purpose +-------- | ------- +`--no-terminal` | SMAPI won't write anything to the console window. (Messages will still be written to the log file.) +`--mods-path` | The path to search for mods, if not the standard `Mods` folder. This can be a path relative to the game folder (like `--mods-path "Mods (test)"`) or an absolute path. + +### Compile flags +SMAPI uses a small number of conditional compilation constants, which you can set by editing the +`` element in `StardewModdingAPI.csproj`. Supported constants: + +flag | purpose +---- | ------- +`SMAPI_FOR_WINDOWS` | Whether SMAPI is being compiled on Windows for players on Windows. Set automatically in `crossplatform.targets`. + +## For SMAPI developers +### Compiling from source +Using an official SMAPI release is recommended for most users. + +SMAPI uses some C# 7 code, so you'll need at least +[Visual Studio 2017](https://www.visualstudio.com/vs/community/) on Windows, +[MonoDevelop 7.0](https://www.monodevelop.com/) on Linux, +[Visual Studio 2017 for Mac](https://www.visualstudio.com/vs/visual-studio-mac/), or an equivalent +IDE to compile it. It uses build configuration derived from the +[crossplatform mod config](https://github.com/Pathoschild/Stardew.ModBuildConfig#readme) to detect +your current OS automatically and load the correct references. Compile output will be placed in a +`bin` folder at the root of the git repository. + +### Debugging a local build +Rebuilding the solution in debug mode will copy the SMAPI files into your game folder. Starting +the `StardewModdingAPI` project with debugging from Visual Studio (on Mac or Windows) will launch +SMAPI with the debugger attached, so you can intercept errors and step through the code being +executed. This doesn't work in MonoDevelop on Linux, unfortunately. + +### Preparing a release +To prepare a crossplatform SMAPI release, you'll need to compile it on two platforms. See +[crossplatforming info](https://stardewvalleywiki.com/Modding:Modder_Guide/Test_and_Troubleshoot#Testing_on_all_platforms) +on the wiki for the first-time setup. + +1. Update the version number in `GlobalAssemblyInfo.cs` and `Constants::Version`. Make sure you use a + [semantic version](https://semver.org). Recommended format: + + build type | format | example + :--------- | :----------------------- | :------ + dev build | `-alpha.` | `3.0-alpha.20171230` + prerelease | `-beta.` | `3.0-beta.2` + release | `` | `3.0` + +2. In Windows: + 1. Rebuild the solution in Release mode. + 2. Copy `windows-install.*` from `bin/SMAPI installer` and `bin/SMAPI installer for developers` to + Linux/Mac. + +3. In Linux/Mac: + 1. Rebuild the solution in Release mode. + 2. Add the `windows-install.*` files to the `bin/SMAPI installer` and + `bin/SMAPI installer for developers` folders. + 3. Rename the folders to `SMAPI installer` and `SMAPI installer for developers`. + 4. Zip the two folders. + +## Release notes +See [release notes](../release-notes.md). diff --git a/docs/technical/web.md b/docs/technical/web.md new file mode 100644 index 00000000..c8888623 --- /dev/null +++ b/docs/technical/web.md @@ -0,0 +1,106 @@ +← [README](../README.md) + +**SMAPI.Web** contains the code for the `smapi.io` website, including the mod compatibility list +and update check API. + +## Contents +* [Overview](#overview) + * [Log parser](#log-parser) + * [Web API](#web-api) +* [For SMAPI developers](#for-smapi-developers) + * [Local development](#local-development) + * [Deploying to Amazon Beanstalk](#deploying-to-amazon-beanstalk) + +## Overview +The `StardewModdingAPI.Web` project provides an API and web UI hosted at `*.smapi.io`. + +### Log parser +The log parser provides a web UI for uploading, parsing, and sharing SMAPI logs. The logs are +persisted in a compressed form to Pastebin. + +The log parser lives at https://log.smapi.io. + +### Web API +SMAPI provides a web API at `api.smapi.io` for use by SMAPI and external tools. The URL includes a +`{version}` token, which is the SMAPI version for backwards compatibility. This API is publicly +accessible but not officially released; it may change at any time. + +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 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. + +``` +POST https://api.smapi.io/v2.0/mods +{ + "mods": [ + { + "id": "Pathoschild.LookupAnything", + "updateKeys": [ "nexus:541", "chucklefish:4250" ] + } + ], + "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: +``` +[ + { + "id": "Pathoschild.LookupAnything", + "main": { + "version": "1.19", + "url": "https://www.nexusmods.com/stardewvalley/mods/541" + }, + "metadata": { + "id": [ + "Pathoschild.LookupAnything", + "LookupAnything" + ], + "name": "Lookup Anything", + "nexusID": 541, + "gitHubRepo": "Pathoschild/StardewMods", + "compatibilityStatus": "Ok", + "compatibilitySummary": "✓ use latest version." + }, + "errors": [] + } +] +``` + +## For SMAPI developers +### Local development +`StardewModdingAPI.Web` is a regular ASP.NET MVC Core app, so you can just launch it from within +Visual Studio to run a local version. + +There are two differences when it's run locally: all endpoints use HTTP instead of HTTPS, and the +subdomain portion becomes a route (e.g. `log.smapi.io` → `localhost:59482/log`). + +Before running it locally, you need to enter your credentials in the `appsettings.Development.json` +file. See the next section for a description of each setting. This file is listed in `.gitignore` +to prevent accidentally committing credentials. + +### Deploying to Amazon Beanstalk +The app can be deployed to a standard Amazon Beanstalk IIS environment. When creating the +environment, make sure to specify the following environment properties: + +property name | description +------------------------------- | ----------------- +`LogParser:PastebinDevKey` | The [Pastebin developer key](https://pastebin.com/api#1) used to authenticate with the Pastebin API. +`LogParser:PastebinUserKey` | The [Pastebin user key](https://pastebin.com/api#8) used to authenticate with the Pastebin API. Can be left blank to post anonymously. +`LogParser:SectionUrl` | The root URL of the log page, like `https://log.smapi.io/`. +`ModUpdateCheck:GitHubPassword` | The password with which to authenticate to GitHub when fetching release info. +`ModUpdateCheck:GitHubUsername` | The username with which to authenticate to GitHub when fetching release info. -- cgit From f18ad1210cd813d6ddff665841ac712d62d18b1f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 6 Jul 2019 21:10:00 -0400 Subject: update project name --- docs/technical/web.md | 4 ++-- src/SMAPI.Toolkit/Properties/AssemblyInfo.cs | 2 +- src/SMAPI.Web/SMAPI.Web.csproj | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) (limited to 'docs/technical') diff --git a/docs/technical/web.md b/docs/technical/web.md index c8888623..978114ef 100644 --- a/docs/technical/web.md +++ b/docs/technical/web.md @@ -12,7 +12,7 @@ and update check API. * [Deploying to Amazon Beanstalk](#deploying-to-amazon-beanstalk) ## Overview -The `StardewModdingAPI.Web` project provides an API and web UI hosted at `*.smapi.io`. +The `SMAPI.Web` project provides an API and web UI hosted at `*.smapi.io`. ### Log parser The log parser provides a web UI for uploading, parsing, and sharing SMAPI logs. The logs are @@ -83,7 +83,7 @@ For example: ## For SMAPI developers ### Local development -`StardewModdingAPI.Web` is a regular ASP.NET MVC Core app, so you can just launch it from within +`SMAPI.Web` is a regular ASP.NET MVC Core app, so you can just launch it from within Visual Studio to run a local version. There are two differences when it's run locally: all endpoints use HTTP instead of HTTPS, and the diff --git a/src/SMAPI.Toolkit/Properties/AssemblyInfo.cs b/src/SMAPI.Toolkit/Properties/AssemblyInfo.cs index 1bb19e8c..ec873f79 100644 --- a/src/SMAPI.Toolkit/Properties/AssemblyInfo.cs +++ b/src/SMAPI.Toolkit/Properties/AssemblyInfo.cs @@ -4,4 +4,4 @@ using System.Runtime.CompilerServices; [assembly: AssemblyTitle("SMAPI.Toolkit")] [assembly: AssemblyDescription("A library which encapsulates mod-handling logic for mod managers and tools. Not intended for use by mods.")] [assembly: InternalsVisibleTo("StardewModdingAPI")] -[assembly: InternalsVisibleTo("StardewModdingAPI.Web")] +[assembly: InternalsVisibleTo("SMAPI.Web")] diff --git a/src/SMAPI.Web/SMAPI.Web.csproj b/src/SMAPI.Web/SMAPI.Web.csproj index 1d8e41c6..b0aaa8a9 100644 --- a/src/SMAPI.Web/SMAPI.Web.csproj +++ b/src/SMAPI.Web/SMAPI.Web.csproj @@ -1,6 +1,7 @@  + StardewModdingAPI.Web netcoreapp2.0 false latest -- cgit 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) --- docs/technical/web.md | 68 +++++--- src/SMAPI.Web/Controllers/ModsController.cs | 37 ++-- .../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 ++++ src/SMAPI.Web/SMAPI.Web.csproj | 4 + src/SMAPI.Web/Startup.cs | 12 ++ src/SMAPI.Web/appsettings.Development.json | 6 + src/SMAPI.Web/appsettings.json | 6 + 12 files changed, 487 insertions(+), 40 deletions(-) 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 'docs/technical') diff --git a/docs/technical/web.md b/docs/technical/web.md index 978114ef..db54a87a 100644 --- a/docs/technical/web.md +++ b/docs/technical/web.md @@ -82,25 +82,49 @@ For example: ``` ## For SMAPI developers -### Local development -`SMAPI.Web` is a regular ASP.NET MVC Core app, so you can just launch it from within -Visual Studio to run a local version. - -There are two differences when it's run locally: all endpoints use HTTP instead of HTTPS, and the -subdomain portion becomes a route (e.g. `log.smapi.io` → `localhost:59482/log`). - -Before running it locally, you need to enter your credentials in the `appsettings.Development.json` -file. See the next section for a description of each setting. This file is listed in `.gitignore` -to prevent accidentally committing credentials. - -### Deploying to Amazon Beanstalk -The app can be deployed to a standard Amazon Beanstalk IIS environment. When creating the -environment, make sure to specify the following environment properties: - -property name | description -------------------------------- | ----------------- -`LogParser:PastebinDevKey` | The [Pastebin developer key](https://pastebin.com/api#1) used to authenticate with the Pastebin API. -`LogParser:PastebinUserKey` | The [Pastebin user key](https://pastebin.com/api#8) used to authenticate with the Pastebin API. Can be left blank to post anonymously. -`LogParser:SectionUrl` | The root URL of the log page, like `https://log.smapi.io/`. -`ModUpdateCheck:GitHubPassword` | The password with which to authenticate to GitHub when fetching release info. -`ModUpdateCheck:GitHubUsername` | The username with which to authenticate to GitHub when fetching release info. +### Local environment +A local environment lets you run a complete copy of the web project (including cache database) on +your machine, with no external dependencies aside from the actual mod sites. + +Initial setup: + +1. [Install MongoDB](https://docs.mongodb.com/manual/administration/install-community/) and add its + `bin` folder to the system PATH. +2. Create a local folder for the MongoDB data (e.g. `C:\dev\smapi-cache`). +3. Enter your credentials in the `appsettings.Development.json` file. You can leave the MongoDB + credentials as-is to use the default local instance; see the next section for the other settings. + +To launch the environment: +1. Launch MongoDB from a terminal (change the data path if applicable): + ```sh + mongod --dbpath C:\dev\smapi-cache + ``` +2. Launch `SMAPI.Web` from Visual Studio to run a local version of the site. + (Local URLs will use HTTP instead of HTTPS, and subdomains will become routes, like + `log.smapi.io` → `localhost:59482/log`.) + +### Production environment +A production environment includes the web servers and cache database hosted online for public +access. This section assumes you're creating a new production environment from scratch (not using +the official live environment). + +Initial setup: + +1. Launch an empty MongoDB server (e.g. using [MongoDB Atlas](https://www.mongodb.com/cloud/atlas)). +2. Create an AWS Beanstalk .NET environment with these environment properties: + + property name | description + ------------------------------- | ----------------- + `LogParser:PastebinDevKey` | The [Pastebin developer key](https://pastebin.com/api#1) used to authenticate with the Pastebin API. + `LogParser:PastebinUserKey` | The [Pastebin user key](https://pastebin.com/api#8) used to authenticate with the Pastebin API. Can be left blank to post anonymously. + `LogParser:SectionUrl` | The root URL of the log page, like `https://log.smapi.io/`. + `ModUpdateCheck:GitHubPassword` | The password with which to authenticate to GitHub when fetching release info. + `ModUpdateCheck:GitHubUsername` | The username with which to authenticate to GitHub when fetching release info. + `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. + +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.) diff --git a/src/SMAPI.Web/Controllers/ModsController.cs b/src/SMAPI.Web/Controllers/ModsController.cs index ca866a8d..b6040e06 100644 --- a/src/SMAPI.Web/Controllers/ModsController.cs +++ b/src/SMAPI.Web/Controllers/ModsController.cs @@ -1,12 +1,10 @@ -using System; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using StardewModdingAPI.Toolkit; -using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; +using StardewModdingAPI.Web.Framework.Caching.Wiki; using StardewModdingAPI.Web.Framework.ConfigModels; using StardewModdingAPI.Web.ViewModels; @@ -19,7 +17,7 @@ namespace StardewModdingAPI.Web.Controllers ** Fields *********/ /// The cache in which to store mod metadata. - private readonly IMemoryCache Cache; + private readonly IWikiCacheRepository Cache; /// The number of minutes successful update checks should be cached before refetching them. private readonly int CacheMinutes; @@ -31,7 +29,7 @@ namespace StardewModdingAPI.Web.Controllers /// Construct an instance. /// The cache in which to store mod metadata. /// The config settings for mod update checks. - public ModsController(IMemoryCache cache, IOptions configProvider) + public ModsController(IWikiCacheRepository cache, IOptions configProvider) { ModCompatibilityListConfig config = configProvider.Value; @@ -54,21 +52,24 @@ namespace StardewModdingAPI.Web.Controllers /// Asynchronously fetch mod metadata from the wiki. public async Task FetchDataAsync() { - return await this.Cache.GetOrCreateAsync($"{nameof(ModsController)}_mod_list", async entry => + // refresh cache + CachedWikiMod[] mods; + if (!this.Cache.TryGetWikiMetadata(out CachedWikiMetadata metadata) || this.Cache.IsStale(metadata.LastUpdated, this.CacheMinutes)) { - WikiModList data = await new ModToolkit().GetWikiCompatibilityListAsync(); - ModListModel model = new ModListModel( - stableVersion: data.StableVersion, - betaVersion: data.BetaVersion, - mods: data - .Mods - .Select(mod => new ModModel(mod)) - .OrderBy(p => Regex.Replace(p.Name.ToLower(), "[^a-z0-9]", "")) // ignore case, spaces, and special characters when sorting - ); + 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(); - entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(this.CacheMinutes); - return model; - }); + // build model + return new ModListModel( + stableVersion: metadata.StableVersion, + betaVersion: metadata.BetaVersion, + mods: mods + .Select(mod => new ModModel(mod.GetModel())) + .OrderBy(p => Regex.Replace(p.Name.ToLower(), "[^a-z0-9]", "")) // ignore case, spaces, and special characters when sorting + ); } } } 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"; + } + } +} diff --git a/src/SMAPI.Web/SMAPI.Web.csproj b/src/SMAPI.Web/SMAPI.Web.csproj index b0aaa8a9..90641611 100644 --- a/src/SMAPI.Web/SMAPI.Web.csproj +++ b/src/SMAPI.Web/SMAPI.Web.csproj @@ -23,12 +23,16 @@ + + + + diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index b409a81d..0a8d23a8 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -6,9 +6,11 @@ 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; using StardewModdingAPI.Web.Framework; +using StardewModdingAPI.Web.Framework.Caching.Wiki; using StardewModdingAPI.Web.Framework.Clients.Chucklefish; using StardewModdingAPI.Web.Framework.Clients.GitHub; using StardewModdingAPI.Web.Framework.Clients.ModDrop; @@ -53,6 +55,7 @@ namespace StardewModdingAPI.Web .Configure(this.Configuration.GetSection("ModCompatibilityList")) .Configure(this.Configuration.GetSection("ModUpdateCheck")) .Configure(this.Configuration.GetSection("Site")) + .Configure(this.Configuration.GetSection("MongoDB")) .Configure(options => options.ConstraintMap.Add("semanticVersion", typeof(VersionConstraint))) .AddMemoryCache() .AddMvc() @@ -108,6 +111,15 @@ namespace StardewModdingAPI.Web devKey: api.PastebinDevKey )); } + + // init MongoDB + { + MongoDbConfig mongoConfig = this.Configuration.GetSection("MongoDB").Get(); + string connectionString = mongoConfig.GetConnectionString("smapi"); + + services.AddSingleton(serv => new MongoClient(connectionString).GetDatabase("smapi")); + services.AddSingleton(serv => new WikiCacheRepository(serv.GetService())); + } } /// The method called by the runtime to configure the HTTP request pipeline. diff --git a/src/SMAPI.Web/appsettings.Development.json b/src/SMAPI.Web/appsettings.Development.json index 49234a3b..9b0ec535 100644 --- a/src/SMAPI.Web/appsettings.Development.json +++ b/src/SMAPI.Web/appsettings.Development.json @@ -31,5 +31,11 @@ "PastebinUserKey": null, "PastebinDevKey": null + }, + + "MongoDB": { + "Host": "localhost", + "Username": null, + "Password": null } } diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json index 9e15aa97..65ccea75 100644 --- a/src/SMAPI.Web/appsettings.json +++ b/src/SMAPI.Web/appsettings.json @@ -47,6 +47,12 @@ "PastebinDevKey": null // see top note }, + "MongoDB": { + "Host": null, // see top note + "Username": null, // see top note + "Password": null // see top note + }, + "ModCompatibilityList": { "CacheMinutes": 10 }, -- 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 'docs/technical') 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 'docs/technical') 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 ee0ff5687d4002aab20cd91fd28d007d916af36c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 4 Aug 2019 18:01:05 -0400 Subject: add user-friendly doc link & error messages, document validator, improve manifest schema (#654) --- docs/technical/web.md | 37 +++++++++++++++- .../Controllers/JsonValidatorController.cs | 49 +++++++++++++++++++++- .../ViewModels/JsonValidator/JsonValidatorModel.cs | 3 ++ src/SMAPI.Web/Views/JsonValidator/Index.cshtml | 5 +++ src/SMAPI.Web/wwwroot/schemas/manifest.json | 48 ++++++++++++++------- 5 files changed, 124 insertions(+), 18 deletions(-) (limited to 'docs/technical') diff --git a/docs/technical/web.md b/docs/technical/web.md index 50799e00..9884fefc 100644 --- a/docs/technical/web.md +++ b/docs/technical/web.md @@ -6,6 +6,7 @@ and update check API. ## Contents * [Overview](#overview) * [Log parser](#log-parser) + * [JSON validator](#json-validator) * [Web API](#web-api) * [For SMAPI developers](#for-smapi-developers) * [Local development](#local-development) @@ -16,9 +17,41 @@ The `SMAPI.Web` project provides an API and web UI hosted at `*.smapi.io`. ### Log parser The log parser provides a web UI for uploading, parsing, and sharing SMAPI logs. The logs are -persisted in a compressed form to Pastebin. +persisted in a compressed form to Pastebin. The log parser lives at https://log.smapi.io. + +### JSON validator +The JSON validator provides a web UI for uploading and sharing JSON files, and validating them +as plain JSON or against a predefined format like `manifest.json` or Content Patcher's +`content.json`. The JSON validator lives at https://json.smapi.io. + +Schema files are defined in `wwwroot/schemas` using the [JSON Schema](https://json-schema.org/) +format, with some special properties: +* The root schema may have a `@documentationURL` field, which is the URL to the user-facing + documentation for the format (if any). +* Any part of the schema can define an `@errorMessages` field, which specifies user-friendly errors + which override the auto-generated messages. These are indexed by error type. For example: + ```js + "pattern": "^[a-zA-Z0-9_.-]+\\.dll$", + "@errorMessages": { + "pattern": "Invalid value; must be a filename ending with .dll." + } + ``` + +You can also reference these schemas in your JSON file directly using the `$schema` field, for +text editors that support schema validation. For example: +```js +{ + "$schema": "https://smapi.io/schemas/manifest.json", + "Name": "Some mod", + ... +} +``` + +Current schemas: -The log parser lives at https://log.smapi.io. +format | schema URL +------ | ---------- +[SMAPI `manifest.json`](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest) | https://smapi.io/schemas/manifest.json ### Web API SMAPI provides a web API at `api.smapi.io` for use by SMAPI and external tools. The URL includes a diff --git a/src/SMAPI.Web/Controllers/JsonValidatorController.cs b/src/SMAPI.Web/Controllers/JsonValidatorController.cs index 37393a98..7b755d3b 100644 --- a/src/SMAPI.Web/Controllers/JsonValidatorController.cs +++ b/src/SMAPI.Web/Controllers/JsonValidatorController.cs @@ -113,6 +113,9 @@ namespace StardewModdingAPI.Web.Controllers schema = JSchema.Parse(System.IO.File.ReadAllText(schemaFile.FullName)); } + // get format doc URL + result.FormatUrl = this.GetExtensionField(schema, "@documentationUrl"); + // validate JSON parsed.IsValid(schema, out IList rawErrors); var errors = rawErrors @@ -172,13 +175,22 @@ namespace StardewModdingAPI.Web.Controllers /// The indentation level to apply for inner errors. private string GetFlattenedError(ValidationError error, int indent = 0) { + // get override error + string message = this.GetOverrideError(error.Schema, error.ErrorType); + if (message != null) + return message; + // get friendly representation of main error - string message = error.Message; + 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; + + case ErrorType.Required: + message = $"Missing required fields: {string.Join(", ", (List)error.Value)}."; + break; } // add inner errors @@ -216,5 +228,40 @@ namespace StardewModdingAPI.Web.Controllers return null; } + + /// Get an override error from the JSON schema, if any. + /// The schema or subschema that raised the error. + /// The error type. + private string GetOverrideError(JSchema schema, ErrorType errorType) + { + // get override errors + IDictionary errors = this.GetExtensionField>(schema, "@errorMessages"); + if (errors == null) + return null; + errors = new Dictionary(errors, StringComparer.InvariantCultureIgnoreCase); + + // get matching error + return errors.TryGetValue(errorType.ToString(), out string errorPhrase) + ? errorPhrase + : null; + } + + /// Get an extension field from a JSON schema. + /// The field type. + /// The schema whose extension fields to search. + /// The case-insensitive field key. + private T GetExtensionField(JSchema schema, string key) + { + if (schema.ExtensionData != null) + { + foreach (var pair in schema.ExtensionData) + { + if (pair.Key.Equals(key, StringComparison.InvariantCultureIgnoreCase)) + return pair.Value.ToObject(); + } + } + + return default; + } } } diff --git a/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorModel.cs b/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorModel.cs index 4c122d4f..2d13bf23 100644 --- a/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorModel.cs +++ b/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorModel.cs @@ -33,6 +33,9 @@ namespace StardewModdingAPI.Web.ViewModels.JsonValidator /// An error which occurred while parsing the JSON. public string ParseError { get; set; } + /// A web URL to the user-facing format documentation. + public string FormatUrl { get; set; } + /********* ** Public methods diff --git a/src/SMAPI.Web/Views/JsonValidator/Index.cshtml b/src/SMAPI.Web/Views/JsonValidator/Index.cshtml index 6658e7b9..5c3168e5 100644 --- a/src/SMAPI.Web/Views/JsonValidator/Index.cshtml +++ b/src/SMAPI.Web/Views/JsonValidator/Index.cshtml @@ -95,6 +95,11 @@ else if (Model.PasteID != null)

Validation errors

+ @if (Model.FormatUrl != null) + { +

See format documentation.

+ } + @if (Model.Errors.Any()) { diff --git a/src/SMAPI.Web/wwwroot/schemas/manifest.json b/src/SMAPI.Web/wwwroot/schemas/manifest.json index 06173333..804eb53d 100644 --- a/src/SMAPI.Web/wwwroot/schemas/manifest.json +++ b/src/SMAPI.Web/wwwroot/schemas/manifest.json @@ -3,6 +3,7 @@ "$id": "https://smapi.io/schemas/manifest.json", "title": "SMAPI manifest", "description": "Manifest file for a SMAPI mod or content pack", + "@documentationUrl": "https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest", "type": "object", "properties": { "Name": { @@ -38,7 +39,10 @@ "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" + "examples": "LookupAnything.dll", + "@errorMessages": { + "pattern": "Invalid value; must be a filename ending with .dll." + } }, "ContentPackFor": { "title": "Content pack for", @@ -59,12 +63,17 @@ "required": [ "UniqueID" ] }, + "MinimumApiVersion": { + "title": "Minimum API version", + "description": "The minimum SMAPI version needed to use this mod. If a player tries to use the mod with an older SMAPI version, they'll see a friendly message saying they need to update SMAPI. This also serves as a proxy for the minimum game version, since SMAPI itself enforces a minimum game version.", + "$ref": "#/definitions/SemanticVersion" + }, "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", + "type": "object", "properties": { "UniqueID": { "title": "Dependency unique ID", @@ -90,8 +99,26 @@ "type": "array", "items": { "type": "string", - "pattern": "^(Chucklefish:\\d+|Nexus:\\d+|GitHub:[A-Za-z0-9_]+/[A-Za-z0-9_]+|ModDrop:\\d+)$" + "pattern": "^(Chucklefish:\\d+|Nexus:\\d+|GitHub:[A-Za-z0-9_]+/[A-Za-z0-9_]+|ModDrop:\\d+)$", + "@errorMessages": { + "pattern": "Invalid update key; see https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest#Update_checks for more info." + } + } + } + }, + "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" ], + "@errorMessages": { + "pattern": "Invalid semantic version; must be formatted like 1.2.0 or 1.2.0-prerelease.tags. See https://semver.org/ for more info." } + }, + "ModID": { + "type": "string", + "pattern": "^[a-zA-Z0-9_.-]+$", // derived from SMAPI.Toolkit.Utilities.PathUtilities.IsSlug + "examples": [ "Pathoschild.LookupAnything" ] } }, @@ -104,17 +131,8 @@ "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" ] - } + "additionalProperties": false, + "@errorMessages": { + "oneOf": "Can't specify both EntryDll or ContentPackFor, they're mutually exclusive." } } -- cgit From 84ad8b2a92eac9155cada821c57d62a517b958a8 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 4 Aug 2019 20:13:10 -0400 Subject: fix manifest error if neither EntryDll nor ContentPackFor are specified (#654) --- docs/technical/web.md | 10 ++++++++- .../Controllers/JsonValidatorController.cs | 26 +++++++++++++++++----- src/SMAPI.Web/wwwroot/schemas/manifest.json | 3 ++- 3 files changed, 31 insertions(+), 8 deletions(-) (limited to 'docs/technical') diff --git a/docs/technical/web.md b/docs/technical/web.md index 9884fefc..0d2039d8 100644 --- a/docs/technical/web.md +++ b/docs/technical/web.md @@ -29,13 +29,21 @@ format, with some special properties: * The root schema may have a `@documentationURL` field, which is the URL to the user-facing documentation for the format (if any). * Any part of the schema can define an `@errorMessages` field, which specifies user-friendly errors - which override the auto-generated messages. These are indexed by error type. For example: + which override the auto-generated messages. These can be indexed by error type: ```js "pattern": "^[a-zA-Z0-9_.-]+\\.dll$", "@errorMessages": { "pattern": "Invalid value; must be a filename ending with .dll." } ``` + ...or by error type and a regular expression applied to the default message (not recommended + unless the previous form doesn't work, since it's more likely to break in future versions): + ```js + "@errorMessages": { + "oneOf:valid against no schemas": "Missing required field: EntryDll or ContentPackFor.", + "oneOf:valid against more than one schema": "Can't specify both EntryDll or ContentPackFor, they're mutually exclusive." + } + ``` You can also reference these schemas in your JSON file directly using the `$schema` field, for text editors that support schema validation. For example: diff --git a/src/SMAPI.Web/Controllers/JsonValidatorController.cs b/src/SMAPI.Web/Controllers/JsonValidatorController.cs index 7b755d3b..b69a1006 100644 --- a/src/SMAPI.Web/Controllers/JsonValidatorController.cs +++ b/src/SMAPI.Web/Controllers/JsonValidatorController.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; @@ -176,7 +177,7 @@ namespace StardewModdingAPI.Web.Controllers private string GetFlattenedError(ValidationError error, int indent = 0) { // get override error - string message = this.GetOverrideError(error.Schema, error.ErrorType); + string message = this.GetOverrideError(error.Schema, error.ErrorType, error.Message); if (message != null) return message; @@ -232,7 +233,8 @@ namespace StardewModdingAPI.Web.Controllers /// Get an override error from the JSON schema, if any. /// The schema or subschema that raised the error. /// The error type. - private string GetOverrideError(JSchema schema, ErrorType errorType) + /// The error message. + private string GetOverrideError(JSchema schema, ErrorType errorType, string message) { // get override errors IDictionary errors = this.GetExtensionField>(schema, "@errorMessages"); @@ -240,10 +242,22 @@ namespace StardewModdingAPI.Web.Controllers return null; errors = new Dictionary(errors, StringComparer.InvariantCultureIgnoreCase); - // get matching error - return errors.TryGetValue(errorType.ToString(), out string errorPhrase) - ? errorPhrase - : null; + // match error by type and message + foreach (var pair in errors) + { + if (!pair.Key.Contains(":")) + continue; + + string[] parts = pair.Key.Split(':', 2); + if (parts[0].Equals(errorType.ToString(), StringComparison.InvariantCultureIgnoreCase) && Regex.IsMatch(message, parts[1])) + return pair.Value; + } + + // match by type + if (errors.TryGetValue(errorType.ToString(), out string error)) + return error; + + return null; } /// Get an extension field from a JSON schema. diff --git a/src/SMAPI.Web/wwwroot/schemas/manifest.json b/src/SMAPI.Web/wwwroot/schemas/manifest.json index 804eb53d..41d1ec2d 100644 --- a/src/SMAPI.Web/wwwroot/schemas/manifest.json +++ b/src/SMAPI.Web/wwwroot/schemas/manifest.json @@ -133,6 +133,7 @@ ], "additionalProperties": false, "@errorMessages": { - "oneOf": "Can't specify both EntryDll or ContentPackFor, they're mutually exclusive." + "oneOf:valid against no schemas": "Missing required field: EntryDll or ContentPackFor.", + "oneOf:valid against more than one schema": "Can't specify both EntryDll or ContentPackFor, they're mutually exclusive." } } -- cgit From 3331beb17a7bda439b281e7507ae187c68b6359c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 5 Aug 2019 23:30:11 -0400 Subject: integrate Content Patcher schema into validator, update docs (#654) --- docs/release-notes.md | 1 + docs/technical/web.md | 1 + .../Controllers/JsonValidatorController.cs | 3 +- src/SMAPI.Web/wwwroot/schemas/content-patcher.json | 33 +++++++++++----------- 4 files changed, 21 insertions(+), 17 deletions(-) (limited to 'docs/technical') diff --git a/docs/release-notes.md b/docs/release-notes.md index 9ec33000..9ce88db1 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -39,6 +39,7 @@ These changes have not been released yet. * For the JSON validator: * Added JSON validator at [json.smapi.io](https://json.smapi.io), which lets you validate a JSON file against predefined mod formats. * Added support for the `manifest.json` format. + * Added support for Content Patcher's `content.json` format (thanks to TehPers!). * 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/docs/technical/web.md b/docs/technical/web.md index 0d2039d8..a008fe72 100644 --- a/docs/technical/web.md +++ b/docs/technical/web.md @@ -60,6 +60,7 @@ Current schemas: format | schema URL ------ | ---------- [SMAPI `manifest.json`](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest) | https://smapi.io/schemas/manifest.json +[Content Patcher `content.json`](https://github.com/Pathoschild/StardewMods/tree/develop/ContentPatcher#readme) | https://smapi.io/schemas/content-patcher.json ### Web API SMAPI provides a web API at `api.smapi.io` for use by SMAPI and external tools. The URL includes a diff --git a/src/SMAPI.Web/Controllers/JsonValidatorController.cs b/src/SMAPI.Web/Controllers/JsonValidatorController.cs index 41c07cee..9ded9c0d 100644 --- a/src/SMAPI.Web/Controllers/JsonValidatorController.cs +++ b/src/SMAPI.Web/Controllers/JsonValidatorController.cs @@ -39,7 +39,8 @@ namespace StardewModdingAPI.Web.Controllers private readonly IDictionary SchemaFormats = new Dictionary { ["none"] = "None", - ["manifest"] = "Manifest" + ["manifest"] = "Manifest", + ["content-patcher"] = "Content Patcher" }; /// The schema ID to use if none was specified. diff --git a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json index e5810871..8dae5a98 100644 --- a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json +++ b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json @@ -3,6 +3,7 @@ "$id": "https://smapi.io/schemas/content-patcher.json", "title": "Content Patcher content pack", "description": "Content Patcher content file for mods", + "@documentationUrl": "https://github.com/Pathoschild/StardewMods/tree/develop/ContentPatcher#readme", "type": "object", "definitions": { "Version": { @@ -15,7 +16,7 @@ "title": "Action", "description": "The kind of change to make.", "type": "string", - "enum": ["Load", "EditImage", "EditData", "EditMap"] + "enum": [ "Load", "EditImage", "EditData", "EditMap" ] }, "Target": { "title": "Target asset", @@ -33,7 +34,7 @@ "anyOf": [ { "type": "string", - "enum": ["true", "false"] + "enum": [ "true", "false" ] }, { "type": "string", @@ -47,7 +48,7 @@ "$ref": "#/definitions/Condition" } }, - "required": ["Action", "Target"], + "required": [ "Action", "Target" ], "allOf": [ { "if": { @@ -105,7 +106,7 @@ "$ref": "#/definitions/FromFile" } }, - "required": ["FromFile"] + "required": [ "FromFile" ] }, "EditImageChange": { "properties": { @@ -126,10 +127,10 @@ "title": "Patch mode", "description": "How to apply FromArea to ToArea. Defaults to Replace.", "type": "string", - "enum": ["Replace", "Overlay"] + "enum": [ "Replace", "Overlay" ] } }, - "required": ["FromFile"] + "required": [ "FromFile" ] }, "EditDataChange": { "properties": { @@ -166,7 +167,7 @@ "type": "string" } }, - "required": ["ID", "BeforeID"], + "required": [ "ID", "BeforeID" ], "additionalProperties": false }, { @@ -178,7 +179,7 @@ "type": "string" } }, - "required": ["ID", "AfterID"], + "required": [ "ID", "AfterID" ], "additionalProperties": false }, { @@ -187,14 +188,14 @@ "ToPosition": { "title": "To position", "description": "Move entry so it's right at this position", - "enum": ["Top", "Bottom"] + "enum": [ "Top", "Bottom" ] } }, - "required": ["ID", "ToPosition"], + "required": [ "ID", "ToPosition" ], "additionalProperties": false } ], - "required": ["ID"] + "required": [ "ID" ] } } } @@ -216,7 +217,7 @@ "$ref": "#/definitions/Rectangle" } }, - "required": ["FromFile", "ToArea"] + "required": [ "FromFile", "ToArea" ] }, "Config": { "type": "object", @@ -250,10 +251,10 @@ "const": false } }, - "required": ["AllowBlank"] + "required": [ "AllowBlank" ] }, "then": { - "required": ["Default"] + "required": [ "Default" ] } } }, @@ -282,7 +283,7 @@ "$ref": "#/definitions/Condition" } }, - "required": ["Name", "Value"] + "required": [ "Name", "Value" ] }, "FromFile": { "title": "Source file", @@ -354,5 +355,5 @@ "format": "uri" } }, - "required": ["Format", "Changes"] + "required": [ "Format", "Changes" ] } -- cgit From e51638948f4355c27d6b3f02d637d4ed754ccb73 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 6 Aug 2019 00:52:47 -0400 Subject: add support for @value token in custom schema errors (#654) --- docs/technical/web.md | 2 + .../Controllers/JsonValidatorController.cs | 64 ++++++++++++++-------- 2 files changed, 43 insertions(+), 23 deletions(-) (limited to 'docs/technical') diff --git a/docs/technical/web.md b/docs/technical/web.md index a008fe72..108e3671 100644 --- a/docs/technical/web.md +++ b/docs/technical/web.md @@ -44,6 +44,8 @@ format, with some special properties: "oneOf:valid against more than one schema": "Can't specify both EntryDll or ContentPackFor, they're mutually exclusive." } ``` + Error messages can optionally include a `@value` token, which will be replaced with the error's + value field (which is usually the original field value). You can also reference these schemas in your JSON file directly using the `$schema` field, for text editors that support schema validation. For example: diff --git a/src/SMAPI.Web/Controllers/JsonValidatorController.cs b/src/SMAPI.Web/Controllers/JsonValidatorController.cs index 9ded9c0d..cc2a7add 100644 --- a/src/SMAPI.Web/Controllers/JsonValidatorController.cs +++ b/src/SMAPI.Web/Controllers/JsonValidatorController.cs @@ -181,7 +181,7 @@ namespace StardewModdingAPI.Web.Controllers private string GetFlattenedError(ValidationError error, int indent = 0) { // get override error - string message = this.GetOverrideError(error.Schema, error.ErrorType, error.Message); + string message = this.GetOverrideError(error); if (message != null) return message; @@ -235,33 +235,37 @@ namespace StardewModdingAPI.Web.Controllers } /// Get an override error from the JSON schema, if any. - /// The schema or subschema that raised the error. - /// The error type. - /// The error message. - private string GetOverrideError(JSchema schema, ErrorType errorType, string message) + /// The schema validation error. + private string GetOverrideError(ValidationError error) { - // get override errors - IDictionary errors = this.GetExtensionField>(schema, "@errorMessages"); - if (errors == null) - return null; - errors = new Dictionary(errors, StringComparer.InvariantCultureIgnoreCase); - - // match error by type and message - foreach (var pair in errors) + string GetRawOverrideError() { - if (!pair.Key.Contains(":")) - continue; + // get override errors + IDictionary errors = this.GetExtensionField>(error.Schema, "@errorMessages"); + if (errors == null) + return null; + errors = new Dictionary(errors, StringComparer.InvariantCultureIgnoreCase); + + // match error by type and message + foreach (var pair in errors) + { + if (!pair.Key.Contains(":")) + continue; - string[] parts = pair.Key.Split(':', 2); - if (parts[0].Equals(errorType.ToString(), StringComparison.InvariantCultureIgnoreCase) && Regex.IsMatch(message, parts[1])) - return pair.Value; - } + string[] parts = pair.Key.Split(':', 2); + if (parts[0].Equals(error.ErrorType.ToString(), StringComparison.InvariantCultureIgnoreCase) && Regex.IsMatch(error.Message, parts[1])) + return pair.Value; + } - // match by type - if (errors.TryGetValue(errorType.ToString(), out string error)) - return error; + // match by type + if (errors.TryGetValue(error.ErrorType.ToString(), out string message)) + return message; - return null; + return null; + } + + return GetRawOverrideError() + ?.Replace("@value", this.FormatValue(error.Value)); } /// Get an extension field from a JSON schema. @@ -281,5 +285,19 @@ namespace StardewModdingAPI.Web.Controllers return default; } + + /// Format a schema value for display. + /// The value to format. + private string FormatValue(object value) + { + switch (value) + { + case List list: + return string.Join(", ", list); + + default: + return value?.ToString() ?? "null"; + } + } } } -- cgit From 674ceea74e74c5b0f432534dba981b5066ea7630 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 6 Aug 2019 03:19:38 -0400 Subject: add support for transparent schema errors (#654) --- docs/release-notes.md | 3 +- docs/technical/web.md | 101 +++++++++++++-------- .../Controllers/JsonValidatorController.cs | 89 +++++++++++------- 3 files changed, 124 insertions(+), 69 deletions(-) (limited to 'docs/technical') diff --git a/docs/release-notes.md b/docs/release-notes.md index 9ce88db1..e253c3c0 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -39,7 +39,8 @@ These changes have not been released yet. * For the JSON validator: * Added JSON validator at [json.smapi.io](https://json.smapi.io), which lets you validate a JSON file against predefined mod formats. * Added support for the `manifest.json` format. - * Added support for Content Patcher's `content.json` format (thanks to TehPers!). + * Added support for the Content Patcher format (thanks to TehPers!). + * 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). diff --git a/docs/technical/web.md b/docs/technical/web.md index 108e3671..91af2f98 100644 --- a/docs/technical/web.md +++ b/docs/technical/web.md @@ -4,50 +4,77 @@ and update check API. ## Contents -* [Overview](#overview) - * [Log parser](#log-parser) - * [JSON validator](#json-validator) - * [Web API](#web-api) +* [Log parser](#log-parser) +* [JSON validator](#json-validator) +* [Web API](#web-api) * [For SMAPI developers](#for-smapi-developers) * [Local development](#local-development) * [Deploying to Amazon Beanstalk](#deploying-to-amazon-beanstalk) -## Overview -The `SMAPI.Web` project provides an API and web UI hosted at `*.smapi.io`. - -### Log parser +## Log parser The log parser provides a web UI for uploading, parsing, and sharing SMAPI logs. The logs are persisted in a compressed form to Pastebin. The log parser lives at https://log.smapi.io. -### JSON validator -The JSON validator provides a web UI for uploading and sharing JSON files, and validating them -as plain JSON or against a predefined format like `manifest.json` or Content Patcher's -`content.json`. The JSON validator lives at https://json.smapi.io. +## JSON validator +### Overview +The JSON validator provides a web UI for uploading and sharing JSON files, and validating them as +plain JSON or against a predefined format like `manifest.json` or Content Patcher's `content.json`. +The JSON validator lives at https://json.smapi.io. +### Schema file format Schema files are defined in `wwwroot/schemas` using the [JSON Schema](https://json-schema.org/) -format, with some special properties: -* The root schema may have a `@documentationURL` field, which is the URL to the user-facing - documentation for the format (if any). -* Any part of the schema can define an `@errorMessages` field, which specifies user-friendly errors - which override the auto-generated messages. These can be indexed by error type: - ```js - "pattern": "^[a-zA-Z0-9_.-]+\\.dll$", - "@errorMessages": { - "pattern": "Invalid value; must be a filename ending with .dll." - } - ``` - ...or by error type and a regular expression applied to the default message (not recommended - unless the previous form doesn't work, since it's more likely to break in future versions): - ```js - "@errorMessages": { - "oneOf:valid against no schemas": "Missing required field: EntryDll or ContentPackFor.", - "oneOf:valid against more than one schema": "Can't specify both EntryDll or ContentPackFor, they're mutually exclusive." - } - ``` - Error messages can optionally include a `@value` token, which will be replaced with the error's - value field (which is usually the original field value). - -You can also reference these schemas in your JSON file directly using the `$schema` field, for +format. The JSON validator UI recognises a superset of the standard fields to change output: + +
+
Documentation URL
+
+ +The root schema may have a `@documentationURL` field, which is a web URL for the user +documentation: +```js +"@documentationUrl": "https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest" +``` + +If present, this is shown in the JSON validator UI. + +
+
Error messages
+
+ +Any part of the schema can define an `@errorMessages` field, which overrides matching schema +errors. You can override by error code (recommended), or by error type and a regex pattern matched +against the error message (more fragile): + +```js +// by error type +"pattern": "^[a-zA-Z0-9_.-]+\\.dll$", +"@errorMessages": { + "pattern": "Invalid value; must be a filename ending with .dll." +} +``` +```js +// by error type + message pattern +"@errorMessages": { + "oneOf:valid against no schemas": "Missing required field: EntryDll or ContentPackFor.", + "oneOf:valid against more than one schema": "Can't specify both EntryDll or ContentPackFor, they're mutually exclusive." +} +``` + +Error messages may contain special tokens: +* `@value` is replaced with the error's value field (which is usually the original field value, but + not always). +* If the validation error has exactly one sub-error and the message is set to `$transparent`, the + sub-error will be displayed instead. (The sub-error itself may be set to `$transparent`, etc.) + +Caveats: +* To override an error from a `then` block, the `@errorMessages` must be inside the `then` block + instead of adjacent. + +
+
+ +### Using a schema file directly +You can reference the validator schemas in your JSON file directly using the `$schema` field, for text editors that support schema validation. For example: ```js { @@ -64,11 +91,13 @@ format | schema URL [SMAPI `manifest.json`](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest) | https://smapi.io/schemas/manifest.json [Content Patcher `content.json`](https://github.com/Pathoschild/StardewMods/tree/develop/ContentPatcher#readme) | https://smapi.io/schemas/content-patcher.json -### Web API +## Web API +### Overview SMAPI provides a web API at `api.smapi.io` for use by SMAPI and external tools. The URL includes a `{version}` token, which is the SMAPI version for backwards compatibility. This API is publicly 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 diff --git a/src/SMAPI.Web/Controllers/JsonValidatorController.cs b/src/SMAPI.Web/Controllers/JsonValidatorController.cs index c23103dd..cd0a6439 100644 --- a/src/SMAPI.Web/Controllers/JsonValidatorController.cs +++ b/src/SMAPI.Web/Controllers/JsonValidatorController.cs @@ -124,7 +124,7 @@ namespace StardewModdingAPI.Web.Controllers // validate JSON parsed.IsValid(schema, out IList rawErrors); var errors = rawErrors - .Select(error => new JsonValidatorErrorModel(error.LineNumber, error.Path, this.GetFlattenedError(error), error.ErrorType)) + .Select(this.GetErrorModel) .ToArray(); return this.View("Index", result.AddErrors(errors)); } @@ -175,35 +175,6 @@ namespace StardewModdingAPI.Web.Controllers 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 override error - string message = this.GetOverrideError(error); - if (message != null) - return message; - - // get friendly representation of main error - 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; - - case ErrorType.Required: - message = $"Missing required fields: {string.Join(", ", (List)error.Value)}."; - 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) @@ -234,6 +205,60 @@ namespace StardewModdingAPI.Web.Controllers return null; } + /// Get a flattened representation representation of a schema validation error and any child errors. + /// The error to represent. + private JsonValidatorErrorModel GetErrorModel(ValidationError error) + { + // skip through transparent errors + while (this.GetOverrideError(error) == "$transparent" && error.ChildErrors.Count == 1) + error = error.ChildErrors[0]; + + // get message + string message = this.GetOverrideError(error); + if (message == null) + message = this.FlattenErrorMessage(error); + + // build model + return new JsonValidatorErrorModel(error.LineNumber, error.Path, message, error.ErrorType); + } + + /// Get a flattened, human-readable message for a schema validation error and any child errors. + /// The error to represent. + /// The indentation level to apply for inner errors. + private string FlattenErrorMessage(ValidationError error, int indent = 0) + { + // get override + string message = this.GetOverrideError(error); + if (message != null) + return message; + + // skip through transparent errors + while (this.GetOverrideError(error) == "$transparent" && error.ChildErrors.Count == 1) + error = error.ChildErrors[0]; + + // get friendly representation of main error + message = error.Message; + switch (error.ErrorType) + { + case ErrorType.Const: + message = $"Invalid value. Found '{error.Value}', but expected '{error.Schema.Const}'."; + break; + + case ErrorType.Enum: + message = $"Invalid value. Found '{error.Value}', but expected one of '{string.Join("', '", error.Schema.Enum)}'."; + break; + + case ErrorType.Required: + message = $"Missing required fields: {string.Join(", ", (List)error.Value)}."; + break; + } + + // add inner errors + foreach (ValidationError childError in error.ChildErrors) + message += "\n" + "".PadLeft(indent * 2, ' ') + $"==> {childError.Path}: " + this.FlattenErrorMessage(childError, indent + 1); + return message; + } + /// Get an override error from the JSON schema, if any. /// The schema validation error. private string GetOverrideError(ValidationError error) @@ -254,12 +279,12 @@ namespace StardewModdingAPI.Web.Controllers string[] parts = pair.Key.Split(':', 2); if (parts[0].Equals(error.ErrorType.ToString(), StringComparison.InvariantCultureIgnoreCase) && Regex.IsMatch(error.Message, parts[1])) - return pair.Value; + return pair.Value?.Trim(); } // match by type if (errors.TryGetValue(error.ErrorType.ToString(), out string message)) - return message; + return message?.Trim(); return null; } -- cgit From 807868f4404d850c29ad8ba4cd69beb9f08cecc6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 6 Aug 2019 03:52:32 -0400 Subject: add support for transparent schema errors with multiple child errors (#654) --- docs/technical/web.md | 27 ++++++++++++++++++---- .../Controllers/JsonValidatorController.cs | 25 ++++++++++++-------- 2 files changed, 39 insertions(+), 13 deletions(-) (limited to 'docs/technical') diff --git a/docs/technical/web.md b/docs/technical/web.md index 91af2f98..c13e24e9 100644 --- a/docs/technical/web.md +++ b/docs/technical/web.md @@ -61,10 +61,29 @@ against the error message (more fragile): ``` Error messages may contain special tokens: -* `@value` is replaced with the error's value field (which is usually the original field value, but - not always). -* If the validation error has exactly one sub-error and the message is set to `$transparent`, the - sub-error will be displayed instead. (The sub-error itself may be set to `$transparent`, etc.) + +* The `@value` token is replaced with the error's value field. This is usually (but not always) the + original field value. +* When an error has child errors, by default they're flattened into one message: + ``` + line | field | error + ---- | ---------- | ------------------------------------------------------------------------- + 4 | Changes[0] | JSON does not match schema from 'then'. + | | ==> Changes[0].ToArea.Y: Invalid type. Expected Integer but got String. + | | ==> Changes[0].ToArea: Missing required fields: Height. + ``` + + If you set the message for an error to `$transparent`, the parent error is omitted entirely and + the child errors are shown instead: + ``` + line | field | error + ---- | ------------------- | ---------------------------------------------- + 8 | Changes[0].ToArea.Y | Invalid type. Expected Integer but got String. + 8 | Changes[0].ToArea | Missing required fields: Height. + ``` + + The child errors themselves may be marked `$transparent`, etc. If an error has no child errors, + this override is ignored. Caveats: * To override an error from a `then` block, the `@errorMessages` must be inside the `then` block diff --git a/src/SMAPI.Web/Controllers/JsonValidatorController.cs b/src/SMAPI.Web/Controllers/JsonValidatorController.cs index cd0a6439..4f234449 100644 --- a/src/SMAPI.Web/Controllers/JsonValidatorController.cs +++ b/src/SMAPI.Web/Controllers/JsonValidatorController.cs @@ -46,6 +46,9 @@ namespace StardewModdingAPI.Web.Controllers /// The schema ID to use if none was specified. private string DefaultSchemaID = "manifest"; + /// A token in an error message which indicates that the child errors should be displayed instead. + private readonly string TransparentToken = "$transparent"; + /********* ** Public methods @@ -124,7 +127,7 @@ namespace StardewModdingAPI.Web.Controllers // validate JSON parsed.IsValid(schema, out IList rawErrors); var errors = rawErrors - .Select(this.GetErrorModel) + .SelectMany(this.GetErrorModels) .ToArray(); return this.View("Index", result.AddErrors(errors)); } @@ -205,21 +208,25 @@ namespace StardewModdingAPI.Web.Controllers return null; } - /// Get a flattened representation representation of a schema validation error and any child errors. + /// Get view models representing a schema validation error and any child errors. /// The error to represent. - private JsonValidatorErrorModel GetErrorModel(ValidationError error) + private IEnumerable GetErrorModels(ValidationError error) { // skip through transparent errors - while (this.GetOverrideError(error) == "$transparent" && error.ChildErrors.Count == 1) - error = error.ChildErrors[0]; + if (this.GetOverrideError(error) == this.TransparentToken && error.ChildErrors.Any()) + { + foreach (var model in error.ChildErrors.SelectMany(this.GetErrorModels)) + yield return model; + yield break; + } // get message string message = this.GetOverrideError(error); - if (message == null) + if (message == null || message == this.TransparentToken) message = this.FlattenErrorMessage(error); // build model - return new JsonValidatorErrorModel(error.LineNumber, error.Path, message, error.ErrorType); + yield return new JsonValidatorErrorModel(error.LineNumber, error.Path, message, error.ErrorType); } /// Get a flattened, human-readable message for a schema validation error and any child errors. @@ -229,11 +236,11 @@ namespace StardewModdingAPI.Web.Controllers { // get override string message = this.GetOverrideError(error); - if (message != null) + if (message != null && message != this.TransparentToken) return message; // skip through transparent errors - while (this.GetOverrideError(error) == "$transparent" && error.ChildErrors.Count == 1) + while (this.GetOverrideError(error) == this.TransparentToken && error.ChildErrors.Count == 1) error = error.ChildErrors[0]; // get friendly representation of main error -- cgit From 6036fbf0500795109b1c5dcf53162cb55834d35a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 6 Aug 2019 05:14:02 -0400 Subject: make 'then' blocks transparent by default (#654) --- docs/technical/web.md | 4 +--- src/SMAPI.Web/Controllers/JsonValidatorController.cs | 17 +++++++++++++++-- src/SMAPI.Web/wwwroot/schemas/content-patcher.json | 9 --------- 3 files changed, 16 insertions(+), 14 deletions(-) (limited to 'docs/technical') diff --git a/docs/technical/web.md b/docs/technical/web.md index c13e24e9..27834a4f 100644 --- a/docs/technical/web.md +++ b/docs/technical/web.md @@ -85,9 +85,7 @@ Error messages may contain special tokens: The child errors themselves may be marked `$transparent`, etc. If an error has no child errors, this override is ignored. -Caveats: -* To override an error from a `then` block, the `@errorMessages` must be inside the `then` block - instead of adjacent. + Validation errors for `then` blocks are transparent by default, unless overridden. diff --git a/src/SMAPI.Web/Controllers/JsonValidatorController.cs b/src/SMAPI.Web/Controllers/JsonValidatorController.cs index 4f234449..d82765e7 100644 --- a/src/SMAPI.Web/Controllers/JsonValidatorController.cs +++ b/src/SMAPI.Web/Controllers/JsonValidatorController.cs @@ -213,7 +213,7 @@ namespace StardewModdingAPI.Web.Controllers private IEnumerable GetErrorModels(ValidationError error) { // skip through transparent errors - if (this.GetOverrideError(error) == this.TransparentToken && error.ChildErrors.Any()) + if (this.IsTransparentError(error)) { foreach (var model in error.ChildErrors.SelectMany(this.GetErrorModels)) yield return model; @@ -240,7 +240,7 @@ namespace StardewModdingAPI.Web.Controllers return message; // skip through transparent errors - while (this.GetOverrideError(error) == this.TransparentToken && error.ChildErrors.Count == 1) + if (this.IsTransparentError(error)) error = error.ChildErrors[0]; // get friendly representation of main error @@ -266,6 +266,19 @@ namespace StardewModdingAPI.Web.Controllers return message; } + /// Get whether a validation error should be omitted in favor of its child errors in user-facing error messages. + /// The error to check. + private bool IsTransparentError(ValidationError error) + { + if (!error.ChildErrors.Any()) + return false; + + string @override = this.GetOverrideError(error); + return + @override == this.TransparentToken + || (error.ErrorType == ErrorType.Then && @override == null); + } + /// Get an override error from the JSON schema, if any. /// The schema validation error. private string GetOverrideError(ValidationError error) diff --git a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json index c7f882bc..315a1fb2 100644 --- a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json +++ b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json @@ -273,9 +273,6 @@ "required": [ "FromFile" ], "propertyNames": { "enum": [ "Action", "Target", "LogName", "Enabled", "When", "FromFile" ] - }, - "@errorMessages": { - "then": "$transparent" } } }, @@ -289,9 +286,6 @@ "required": [ "FromFile" ], "propertyNames": { "enum": [ "Action", "Target", "LogName", "Enabled", "When", "FromFile", "FromArea", "ToArea", "PatchMode" ] - }, - "@errorMessages": { - "then": "$transparent" } } }, @@ -304,9 +298,6 @@ "then": { "propertyNames": { "enum": [ "Action", "Target", "LogName", "Enabled", "When", "Fields", "Entries", "MoveEntries" ] - }, - "@errorMessages": { - "then": "$transparent" } } }, -- cgit From 3f6865e8301535c8fbe83bc0f931a116adac0636 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 6 Aug 2019 13:35:20 -0400 Subject: add footer tip about using schema directly, add details to page title (#654) --- docs/technical/web.md | 2 +- src/SMAPI.Web/Views/JsonValidator/Index.cshtml | 27 +++++++++++++++++----- src/SMAPI.Web/Views/Shared/_Layout.cshtml | 2 +- .../wwwroot/Content/css/json-validator.css | 9 ++++++++ 4 files changed, 32 insertions(+), 8 deletions(-) (limited to 'docs/technical') diff --git a/docs/technical/web.md b/docs/technical/web.md index 27834a4f..8fd99f82 100644 --- a/docs/technical/web.md +++ b/docs/technical/web.md @@ -101,7 +101,7 @@ text editors that support schema validation. For example: } ``` -Current schemas: +Available schemas: format | schema URL ------ | ---------- diff --git a/src/SMAPI.Web/Views/JsonValidator/Index.cshtml b/src/SMAPI.Web/Views/JsonValidator/Index.cshtml index 34c1c1f3..3143fad9 100644 --- a/src/SMAPI.Web/Views/JsonValidator/Index.cshtml +++ b/src/SMAPI.Web/Views/JsonValidator/Index.cshtml @@ -2,12 +2,22 @@ @model JsonValidatorModel @{ - ViewData["Title"] = "JSON validator"; - + // get view data string curPageUrl = new Uri(new Uri(Model.SectionUrl), $"{Model.SchemaName}/{Model.PasteID}").ToString(); - string newUploadUrl = Model.SchemaName != null - ? new Uri(new Uri(Model.SectionUrl), Model.SchemaName).ToString() - : Model.SectionUrl; + string newUploadUrl = Model.SchemaName != null ? new Uri(new Uri(Model.SectionUrl), Model.SchemaName).ToString() : Model.SectionUrl; + string schemaDisplayName = null; + bool isValidSchema = Model.SchemaName != null && Model.SchemaFormats.TryGetValue(Model.SchemaName, out schemaDisplayName) && schemaDisplayName != "None"; + + // build title + ViewData["Title"] = "JSON validator"; + @if (Model.PasteID != null) + { + ViewData["ViewTitle"] = ViewData["Title"]; + ViewData["Title"] += + " (" + + string.Join(", ", new[] { isValidSchema ? schemaDisplayName : null, Model.PasteID }.Where(p => p != null)) + + ")"; + } } @section Head { @@ -130,7 +140,12 @@ else if (Model.PasteID != null) } } -

Raw content

+

Content

@Model.Content
+ + @if (isValidSchema) + { + + } } diff --git a/src/SMAPI.Web/Views/Shared/_Layout.cshtml b/src/SMAPI.Web/Views/Shared/_Layout.cshtml index 9911ef0e..87a22f06 100644 --- a/src/SMAPI.Web/Views/Shared/_Layout.cshtml +++ b/src/SMAPI.Web/Views/Shared/_Layout.cshtml @@ -28,7 +28,7 @@
-

@ViewData["Title"]

+

@(ViewData["ViewTitle"] ?? ViewData["Title"])

@RenderBody()
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
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 From 730d9783959cc35945841ab721c930fad8ff9ca0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 23 Nov 2019 22:39:57 -0500 Subject: drop mod build package change which sets the x86 platform Visual Studio changes platform inconsistently when set through a NuGet package, which can cause confusing behavior. It's better to set it directly in the project file instead. --- docs/technical/mod-package.md | 11 +++-------- src/SMAPI.ModBuildConfig/build/smapi.targets | 4 ---- src/SMAPI.ModBuildConfig/package.nuspec | 6 ++---- 3 files changed, 5 insertions(+), 16 deletions(-) (limited to 'docs/technical') diff --git a/docs/technical/mod-package.md b/docs/technical/mod-package.md index 43682bfb..a33480ad 100644 --- a/docs/technical/mod-package.md +++ b/docs/technical/mod-package.md @@ -130,11 +130,8 @@ To disable game debugging (only needed for some non-mod projects): ``` ### Preconfigure common settings -The package automatically enables PDB files, so error logs show line numbers for simpler debugging. - -For projects using the simplified `.csproj` format, it also enables the GAC (to support XNA -Framework) and sets the build to x86 mode (to avoid 'mismatch between the processor architecture' warnings due to - the game being x86). +The package also automatically enables PDB files (so error logs show line numbers for simpler +debugging), and enables support for the simplified `.csproj` format. ### Add code warnings The package runs code analysis on your mod and raises warnings for some common errors or pitfalls. @@ -294,9 +291,7 @@ which can be uploaded to NuGet or referenced directly. * Updated for SMAPI 3.0 and Stardew Valley 1.4. * Added automatic support for `assets` folders. * Added `$(GameExecutableName)` MSBuild variable. -* Added support for projects using the simplified `.csproj` format: - * platform target is now set to x86 automatically to avoid mismatching platform target warnings; - * added GAC to assembly search paths to fix references to XNA Framework. +* Added support for projects using the simplified `.csproj` format. * Added option to disable game debugging config. * Added `.pdb` files to builds by default (to enable line numbers in error stack traces). * Added optional Harmony reference. diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets index 78d3a3d4..5ca9f032 100644 --- a/src/SMAPI.ModBuildConfig/build/smapi.targets +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -8,10 +8,6 @@ ** Set build options **********************************************--> - - x86 - x86 - pdbonly true diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec index 76818c9f..812e5bcb 100644 --- a/src/SMAPI.ModBuildConfig/package.nuspec +++ b/src/SMAPI.ModBuildConfig/package.nuspec @@ -2,7 +2,7 @@ Pathoschild.Stardew.ModBuildConfig - 3.0.0-beta.6 + 3.0.0 Build package for SMAPI mods Pathoschild Pathoschild @@ -17,9 +17,7 @@ - Updated for SMAPI 3.0 and Stardew Valley 1.4. - Added automatic support for 'assets' folders. - Added $(GameExecutableName) MSBuild variable. - - Added support for projects using the simplified .csproj format: - - platform target is now set to x86 automatically to avoid mismatching platform target warnings; - - added GAC to assembly search paths to fix references to XNA Framework. + - Added support for projects using the simplified .csproj format. - Added option to disable game debugging config. - Added .pdb files to builds by default (to enable line numbers in error stack traces). - Added optional Harmony reference. -- cgit