Merge branch 'develop' into stable
This commit is contained in:
commit
d34f369d35
|
@ -24,7 +24,7 @@ Exact steps which reproduce the bug, if possible. For example:
|
|||
4. Error occurs.
|
||||
|
||||
**Log file**
|
||||
Upload your SMAPI log to https://log.smapi.io and post a link here.
|
||||
Upload your SMAPI log to https://smapi.io/log and post a link here.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
|
|
@ -28,4 +28,7 @@ _ReSharper*/
|
|||
appsettings.Development.json
|
||||
|
||||
# AWS generated files
|
||||
src/SMAPI.Web/aws-beanstalk-tools-defaults.json
|
||||
src/SMAPI.Web.LegacyRedirects/aws-beanstalk-tools-defaults.json
|
||||
|
||||
# Azure generated files
|
||||
src/SMAPI.Web/Properties/PublishProfiles/smapi-web-release - Web Deploy.pubxml
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
<!--set properties -->
|
||||
<PropertyGroup>
|
||||
<Version>3.0.0</Version>
|
||||
<Version>3.0.1</Version>
|
||||
<Product>SMAPI</Product>
|
||||
|
||||
<AssemblySearchPaths>$(AssemblySearchPaths);{GAC}</AssemblySearchPaths>
|
||||
|
|
|
@ -63,7 +63,7 @@
|
|||
<Copy SourceFiles="$(CompiledModsPath)\SaveBackup\SaveBackup.pdb" DestinationFolder="$(PackagePath)\bundle\Mods\SaveBackup" />
|
||||
<Copy SourceFiles="$(CompiledModsPath)\SaveBackup\manifest.json" DestinationFolder="$(PackagePath)\bundle\Mods\SaveBackup" />
|
||||
|
||||
<!-- fix errors on Linux/Mac (sample: https://log.smapi.io/mMdFUpgB) -->
|
||||
<!-- fix errors on Linux/Mac (sample: https://smapi.io/log/mMdFUpgB) -->
|
||||
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(RootPath)\build\lib\System.Numerics.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" />
|
||||
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(RootPath)\build\lib\System.Runtime.Caching.dll" DestinationFolder="$(PackagePath)\bundle\smapi-internal" />
|
||||
|
||||
|
|
|
@ -63,7 +63,7 @@ contributing translations.
|
|||
locale | status
|
||||
---------- | :----------------
|
||||
default | ✓ [fully translated](../src/SMAPI/i18n/default.json)
|
||||
Chinese | ❑ not translated
|
||||
Chinese | ✓ [fully translated](../src/SMAPI/i18n/zh.json)
|
||||
French | ❑ not translated
|
||||
German | ✓ [fully translated](../src/SMAPI/i18n/de.json)
|
||||
Hungarian | ❑ not translated
|
||||
|
@ -71,6 +71,6 @@ Italian | ❑ not translated
|
|||
Japanese | ❑ not translated
|
||||
Korean | ❑ not translated
|
||||
Portuguese | ❑ not translated
|
||||
Russian | ❑ not translated
|
||||
Russian | ✓ [fully translated](../src/SMAPI/i18n/ru.json)
|
||||
Spanish | ❑ not translated
|
||||
Turkish | ❑ not translated
|
||||
Turkish | ✓ [fully translated](../src/SMAPI/i18n/tr.json)
|
||||
|
|
|
@ -1,6 +1,22 @@
|
|||
← [README](README.md)
|
||||
|
||||
# Release notes
|
||||
## 3.0.1
|
||||
Released 02 December 2019 for Stardew Valley 1.4.0.1.
|
||||
|
||||
* For players:
|
||||
* Updated for Stardew Valley 1.4.0.1.
|
||||
* Improved compatibility with some Linux terminals (thanks to archification and DanielHeath!).
|
||||
* Updated translations. Thanks to berkayylmao (added Turkish), feathershine (added Chinese), and Osiris901 (added Russian)!
|
||||
|
||||
* For the web UI:
|
||||
* Rebuilt web infrastructure to handle higher traffic.
|
||||
* If a log can't be uploaded to Pastebin (e.g. due to rate limits), it's now uploaded to Amazon S3 instead. Logs uploaded to S3 expire after one month.
|
||||
* Fixed JSON validator not letting you drag & drop a file.
|
||||
|
||||
* For modders:
|
||||
* `SemanticVersion` now supports [semver 2.0](https://semver.org/) build metadata.
|
||||
|
||||
## 3.0
|
||||
Released 26 November 2019 for Stardew Valley 1.4.
|
||||
|
||||
|
@ -80,6 +96,7 @@ For modders:
|
|||
* Update checks are now faster in some cases.
|
||||
* Updated mod compatibility list.
|
||||
* Updated SMAPI/game version map.
|
||||
* Updated translations. Thanks to eren-kemer (added German)!
|
||||
* Fixes:
|
||||
* Fixed some assets not updated when you switch language to English.
|
||||
* Fixed lag in some cases due to incorrect asset caching when playing in non-English.
|
||||
|
@ -102,7 +119,7 @@ For modders:
|
|||
* Clicking a mod link now automatically adds it to the visible mods if the list is filtered.
|
||||
|
||||
* 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 JSON validator at [smapi.io/json](https://smapi.io/json), which lets you validate a JSON file against predefined mod formats.
|
||||
* Added support for the `manifest.json` format.
|
||||
* Added support for the Content Patcher format (thanks to TehPers!).
|
||||
* Added support for referencing a schema in a JSON Schema-compatible text editor.
|
||||
|
@ -365,7 +382,7 @@ Released 19 November 2018 for Stardew Valley 1.3.32.
|
|||
* Updated compatibility list.
|
||||
|
||||
* For the web UI:
|
||||
* Added a [mod compatibility page](https://mods.smapi.io) and [privacy page](https://smapi.io/privacy).
|
||||
* Added a [mod compatibility page](https://smapi.io/mods) and [privacy page](https://smapi.io/privacy).
|
||||
* The log parser now has a separate filter for game messages.
|
||||
* The log parser now shows content pack authors (thanks to danvolchek!).
|
||||
* Tweaked log parser UI (thanks to danvolchek!).
|
||||
|
@ -549,7 +566,7 @@ Released 11 April 2018 for Stardew Valley 1.2.30–1.2.33.
|
|||
* Fixed error when two content packs use different capitalization for the same required mod ID.
|
||||
* Fixed rare crash if the game duplicates an item.
|
||||
|
||||
* For the [log parser](https://log.smapi.io):
|
||||
* For the [log parser](https://smapi.io/log):
|
||||
* Tweaked UI.
|
||||
|
||||
## 2.5.4
|
||||
|
@ -561,7 +578,7 @@ Released 26 March 2018 for Stardew Valley 1.2.30–1.2.33.
|
|||
* Fixed error when mods remove an asset editor/loader.
|
||||
* Fixed minimum game version incorrectly increased in SMAPI 2.5.3.
|
||||
|
||||
* For the [log parser](https://log.smapi.io):
|
||||
* For the [log parser](https://smapi.io/log):
|
||||
* Fixed error when log text contains certain tokens.
|
||||
|
||||
* For modders:
|
||||
|
@ -583,7 +600,7 @@ Released 13 March 2018 for Stardew Valley ~~1.2.30~~–1.2.33.
|
|||
* Fixed Linux ["magic number is wrong" errors](https://github.com/mono/mono/issues/6752) by changing default terminal order.
|
||||
* Updated compatibility list and added update checks for more mods.
|
||||
|
||||
* For the [log parser](https://log.smapi.io):
|
||||
* For the [log parser](https://smapi.io/log):
|
||||
* Fixed incorrect filtering in some cases.
|
||||
* Fixed error if mods have duplicate names.
|
||||
* Fixed parse bugs if a mod has no author name.
|
||||
|
@ -597,7 +614,7 @@ Released 25 February 2018 for Stardew Valley 1.2.30–1.2.33.
|
|||
* For modders:
|
||||
* Fixed issue where replacing an asset through `asset.AsImage()` or `asset.AsDictionary()` didn't take effect.
|
||||
|
||||
* For the [log parser](https://log.smapi.io):
|
||||
* For the [log parser](https://smapi.io/log):
|
||||
* Fixed blank page after uploading a log in some cases.
|
||||
|
||||
## 2.5.1
|
||||
|
@ -628,7 +645,7 @@ Released 24 February 2018 for Stardew Valley 1.2.30–1.2.33.
|
|||
* Fixed unhelpful error when a translation file has duplicate keys due to case-insensitivity.
|
||||
* Fixed some JSON field names being case-sensitive.
|
||||
|
||||
* For the [log parser](https://log.smapi.io):
|
||||
* For the [log parser](https://smapi.io/log):
|
||||
* Added support for SMAPI 2.5 content packs.
|
||||
* Reduced download size when viewing a parsed log with repeated errors.
|
||||
* Improved parse error handling.
|
||||
|
@ -649,7 +666,7 @@ Released 24 January 2018 for Stardew Valley 1.2.30–1.2.33.
|
|||
* Fixed intermittent errors (e.g. 'collection has been modified') with some mods when loading a save.
|
||||
* Fixed compatibility with Linux Terminator terminal.
|
||||
|
||||
* For the [log parser](https://log.smapi.io):
|
||||
* For the [log parser](https://smapi.io/log):
|
||||
* Fixed error parsing logs with zero installed mods.
|
||||
|
||||
* For modders:
|
||||
|
@ -684,7 +701,7 @@ Released 26 December 2017 for Stardew Valley 1.2.30–1.2.33.
|
|||
* Fixed issue where a mod could change the cursor position reported to other mods.
|
||||
* Updated compatibility list.
|
||||
|
||||
* For the [log parser](https://log.smapi.io):
|
||||
* For the [log parser](https://smapi.io/log):
|
||||
* Fixed broken favicon.
|
||||
|
||||
## 2.2
|
||||
|
@ -698,7 +715,7 @@ Released 02 December 2017 for Stardew Valley 1.2.30–1.2.33.
|
|||
* Improved error when a mod has an invalid `EntryDLL` filename format.
|
||||
* Updated compatibility list.
|
||||
|
||||
* For the [log parser](https://log.smapi.io):
|
||||
* For the [log parser](https://smapi.io/log):
|
||||
* Logs no longer expire after a week.
|
||||
* Fixed error when uploading very large logs.
|
||||
* Slightly improved the UI.
|
||||
|
@ -713,7 +730,7 @@ Released 02 December 2017 for Stardew Valley 1.2.30–1.2.33.
|
|||
Released 01 November 2017 for Stardew Valley 1.2.30–1.2.33.
|
||||
|
||||
* For players:
|
||||
* Added a [log parser](https://log.smapi.io) site.
|
||||
* Added a [log parser](https://smapi.io/log) site.
|
||||
* Added better Steam instructions to the SMAPI installer.
|
||||
* Renamed the bundled _TrainerMod_ to _ConsoleCommands_ to make its purpose clearer.
|
||||
* Removed the game's test messages from the console log.
|
||||
|
|
|
@ -14,13 +14,13 @@ and update check API.
|
|||
|
||||
## 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.
|
||||
persisted in a compressed form to Pastebin. The log parser lives at https://smapi.io/log.
|
||||
|
||||
## 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.
|
||||
The JSON validator lives at https://smapi.io/json.
|
||||
|
||||
### Schema file format
|
||||
Schema files are defined in `wwwroot/schemas` using the [JSON Schema](https://json-schema.org/)
|
||||
|
@ -111,7 +111,7 @@ format | schema URL
|
|||
|
||||
## Web API
|
||||
### Overview
|
||||
SMAPI provides a web API at `api.smapi.io` for use by SMAPI and external tools. The URL includes a
|
||||
SMAPI provides a web API at `smapi.io/api` 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.
|
||||
|
||||
|
@ -184,7 +184,7 @@ may be useful to external tools.
|
|||
|
||||
Example request:
|
||||
```js
|
||||
POST https://api.smapi.io/v3.0/mods
|
||||
POST https://smapi.io/api/v3.0/mods
|
||||
{
|
||||
"mods": [
|
||||
{
|
||||
|
@ -350,8 +350,7 @@ To launch the environment:
|
|||
mongod --dbpath C:\dev\smapi-cache
|
||||
```
|
||||
2. Launch `SMAPI.Web` from Visual Studio to run a local version of the site.
|
||||
<small>(Local URLs will use HTTP instead of HTTPS, and subdomains will become routes, like
|
||||
`log.smapi.io` → `localhost:59482/log`.)</small>
|
||||
<small>(Local URLs will use HTTP instead of HTTPS.)</small>
|
||||
|
||||
### Production environment
|
||||
A production environment includes the web servers and cache database hosted online for public
|
||||
|
@ -367,7 +366,6 @@ Initial setup:
|
|||
------------------------------- | -----------------
|
||||
`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.
|
||||
|
|
|
@ -12,6 +12,9 @@ elif type type >/dev/null 2>&1; then
|
|||
COMMAND="type"
|
||||
fi
|
||||
|
||||
# if $TERM is not set to xterm, mono will bail out when attempting to write to the console.
|
||||
export TERM=xterm
|
||||
|
||||
# validate Mono & run installer
|
||||
if $COMMAND mono >/dev/null 2>&1; then
|
||||
mono internal/unix-install.exe
|
||||
|
|
|
@ -62,7 +62,7 @@ else
|
|||
fi
|
||||
|
||||
# select terminal (prefer $TERMINAL for overrides and testing, then xterm for best compatibility, then known supported terminals)
|
||||
for terminal in "$TERMINAL" xterm gnome-terminal kitty terminator xfce4-terminal konsole terminal termite x-terminal-emulator; do
|
||||
for terminal in "$TERMINAL" xterm gnome-terminal kitty terminator xfce4-terminal konsole terminal termite alacritty x-terminal-emulator; do
|
||||
if $COMMAND "$terminal" 2>/dev/null; then
|
||||
# Find the true shell behind x-terminal-emulator
|
||||
if [ "$(basename "$(readlink -f $(which "$terminal"))")" != "x-terminal-emulator" ]; then
|
||||
|
@ -100,6 +100,14 @@ else
|
|||
# Kitty overrides the TERM varible unless you set it explicitly
|
||||
kitty -o term=xterm $LAUNCHER
|
||||
;;
|
||||
alacritty)
|
||||
# Alacritty doesn't like the double quotes or the variable
|
||||
if [ "$ARCH" == "x86_64" ]; then
|
||||
alacritty -e sh -c 'TERM=xterm ./StardewModdingAPI.bin.x86_64 $*'
|
||||
else
|
||||
alacritty -e sh -c 'TERM=xterm ./StardewModdingAPI.bin.x86 $*'
|
||||
fi
|
||||
;;
|
||||
xterm|xfce4-terminal|gnome-terminal|terminal|termite)
|
||||
$LAUNCHTERM -e "sh -c 'TERM=xterm $LAUNCHER'"
|
||||
;;
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"Name": "Console Commands",
|
||||
"Author": "SMAPI",
|
||||
"Version": "3.0.0",
|
||||
"Version": "3.0.1",
|
||||
"Description": "Adds SMAPI console commands that let you manipulate the game.",
|
||||
"UniqueID": "SMAPI.ConsoleCommands",
|
||||
"EntryDll": "ConsoleCommands.dll",
|
||||
"MinimumApiVersion": "3.0.0"
|
||||
"MinimumApiVersion": "3.0.1"
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"Name": "Save Backup",
|
||||
"Author": "SMAPI",
|
||||
"Version": "3.0.0",
|
||||
"Version": "3.0.1",
|
||||
"Description": "Automatically backs up all your saves once per day into its folder.",
|
||||
"UniqueID": "SMAPI.SaveBackup",
|
||||
"EntryDll": "SaveBackup.dll",
|
||||
"MinimumApiVersion": "3.0.0"
|
||||
"MinimumApiVersion": "3.0.1"
|
||||
}
|
||||
|
|
|
@ -25,44 +25,50 @@ namespace SMAPI.Tests.Utilities
|
|||
[TestCase("1.2.3-some-tag.4", ExpectedResult = "1.2.3-some-tag.4")]
|
||||
[TestCase("1.2.3-SoME-tAg.4", ExpectedResult = "1.2.3-SoME-tAg.4")]
|
||||
[TestCase("1.2.3-some-tag.4 ", ExpectedResult = "1.2.3-some-tag.4")]
|
||||
[TestCase("1.2.3-some-tag.4+build.004", ExpectedResult = "1.2.3-some-tag.4+build.004")]
|
||||
[TestCase("1.2+3.4.5-build.004", ExpectedResult = "1.2.0+3.4.5-build.004")]
|
||||
public string Constructor_FromString(string input)
|
||||
{
|
||||
return new SemanticVersion(input).ToString();
|
||||
}
|
||||
|
||||
[Test(Description = "Assert that the constructor sets the expected values for all valid versions when constructed from the individual numbers.")]
|
||||
[TestCase(1, 0, 0, null, ExpectedResult = "1.0.0")]
|
||||
[TestCase(3000, 4000, 5000, null, ExpectedResult = "3000.4000.5000")]
|
||||
[TestCase(1, 2, 3, "", ExpectedResult = "1.2.3")]
|
||||
[TestCase(1, 2, 3, " ", ExpectedResult = "1.2.3")]
|
||||
[TestCase(1, 2, 3, "0", ExpectedResult = "1.2.3-0")]
|
||||
[TestCase(1, 2, 3, "some-tag.4", ExpectedResult = "1.2.3-some-tag.4")]
|
||||
[TestCase(1, 2, 3, "sOMe-TaG.4", ExpectedResult = "1.2.3-sOMe-TaG.4")]
|
||||
[TestCase(1, 2, 3, "some-tag.4 ", ExpectedResult = "1.2.3-some-tag.4")]
|
||||
public string Constructor_FromParts(int major, int minor, int patch, string tag)
|
||||
[TestCase(1, 0, 0, null, null, ExpectedResult = "1.0.0")]
|
||||
[TestCase(3000, 4000, 5000, null, null, ExpectedResult = "3000.4000.5000")]
|
||||
[TestCase(1, 2, 3, "", null, ExpectedResult = "1.2.3")]
|
||||
[TestCase(1, 2, 3, " ", null, ExpectedResult = "1.2.3")]
|
||||
[TestCase(1, 2, 3, "0", null, ExpectedResult = "1.2.3-0")]
|
||||
[TestCase(1, 2, 3, "some-tag.4", null, ExpectedResult = "1.2.3-some-tag.4")]
|
||||
[TestCase(1, 2, 3, "sOMe-TaG.4", null, ExpectedResult = "1.2.3-sOMe-TaG.4")]
|
||||
[TestCase(1, 2, 3, "some-tag.4 ", null, ExpectedResult = "1.2.3-some-tag.4")]
|
||||
[TestCase(1, 2, 3, "some-tag.4 ", "build.004", ExpectedResult = "1.2.3-some-tag.4+build.004")]
|
||||
[TestCase(1, 2, 0, null, "3.4.5-build.004", ExpectedResult = "1.2.0+3.4.5-build.004")]
|
||||
public string Constructor_FromParts(int major, int minor, int patch, string prerelease, string build)
|
||||
{
|
||||
// act
|
||||
ISemanticVersion version = new SemanticVersion(major, minor, patch, tag);
|
||||
ISemanticVersion version = new SemanticVersion(major, minor, patch, prerelease, build);
|
||||
|
||||
// assert
|
||||
Assert.AreEqual(major, version.MajorVersion, "The major version doesn't match the given value.");
|
||||
Assert.AreEqual(minor, version.MinorVersion, "The minor version doesn't match the given value.");
|
||||
Assert.AreEqual(patch, version.PatchVersion, "The patch version doesn't match the given value.");
|
||||
Assert.AreEqual(string.IsNullOrWhiteSpace(tag) ? null : tag.Trim(), version.PrereleaseTag, "The tag doesn't match the given value.");
|
||||
Assert.AreEqual(string.IsNullOrWhiteSpace(prerelease) ? null : prerelease.Trim(), version.PrereleaseTag, "The prerelease tag doesn't match the given value.");
|
||||
Assert.AreEqual(string.IsNullOrWhiteSpace(build) ? null : build.Trim(), version.BuildMetadata, "The build metadata doesn't match the given value.");
|
||||
return version.ToString();
|
||||
}
|
||||
|
||||
[Test(Description = "Assert that the constructor throws the expected exception for invalid versions when constructed from the individual numbers.")]
|
||||
[TestCase(0, 0, 0, null)]
|
||||
[TestCase(-1, 0, 0, null)]
|
||||
[TestCase(0, -1, 0, null)]
|
||||
[TestCase(0, 0, -1, null)]
|
||||
[TestCase(1, 0, 0, "-tag")]
|
||||
[TestCase(1, 0, 0, "tag spaces")]
|
||||
[TestCase(1, 0, 0, "tag~")]
|
||||
public void Constructor_FromParts_WithInvalidValues(int major, int minor, int patch, string tag)
|
||||
[TestCase(0, 0, 0, null, null)]
|
||||
[TestCase(-1, 0, 0, null, null)]
|
||||
[TestCase(0, -1, 0, null, null)]
|
||||
[TestCase(0, 0, -1, null, null)]
|
||||
[TestCase(1, 0, 0, "-tag", null)]
|
||||
[TestCase(1, 0, 0, "tag spaces", null)]
|
||||
[TestCase(1, 0, 0, "tag~", null)]
|
||||
[TestCase(1, 0, 0, null, "build~")]
|
||||
public void Constructor_FromParts_WithInvalidValues(int major, int minor, int patch, string prerelease, string build)
|
||||
{
|
||||
this.AssertAndLogException<FormatException>(() => new SemanticVersion(major, minor, patch, tag));
|
||||
this.AssertAndLogException<FormatException>(() => new SemanticVersion(major, minor, patch, prerelease, build));
|
||||
}
|
||||
|
||||
[Test(Description = "Assert that the constructor sets the expected values for all valid versions when constructed from an assembly version.")]
|
||||
|
@ -98,6 +104,7 @@ namespace SMAPI.Tests.Utilities
|
|||
[TestCase("1.2.3--some-tag")]
|
||||
[TestCase("1.2.3-some-tag...")]
|
||||
[TestCase("1.2.3-some-tag...4")]
|
||||
[TestCase("1.2.3-some-tag.4+build...4")]
|
||||
[TestCase("apple")]
|
||||
[TestCase("-apple")]
|
||||
[TestCase("-5")]
|
||||
|
@ -119,6 +126,8 @@ namespace SMAPI.Tests.Utilities
|
|||
[TestCase("1.0-beta", "1.0-beta", ExpectedResult = 0)]
|
||||
[TestCase("1.0-beta.10", "1.0-beta.10", ExpectedResult = 0)]
|
||||
[TestCase("1.0-beta", "1.0-beta ", ExpectedResult = 0)]
|
||||
[TestCase("1.0-beta+build.001", "1.0-beta+build.001", ExpectedResult = 0)]
|
||||
[TestCase("1.0-beta+build.001", "1.0-beta+build.006", ExpectedResult = 0)] // build metadata must not affect precedence
|
||||
|
||||
// less than
|
||||
[TestCase("0.5.7", "0.5.8", ExpectedResult = -1)]
|
||||
|
@ -156,6 +165,8 @@ namespace SMAPI.Tests.Utilities
|
|||
[TestCase("1.0-beta", "1.0-beta", ExpectedResult = false)]
|
||||
[TestCase("1.0-beta.10", "1.0-beta.10", ExpectedResult = false)]
|
||||
[TestCase("1.0-beta", "1.0-beta ", ExpectedResult = false)]
|
||||
[TestCase("1.0-beta+build.001", "1.0-beta+build.001", ExpectedResult = false)] // build metadata must not affect precedence
|
||||
[TestCase("1.0-beta+build.001", "1.0-beta+build.006", ExpectedResult = false)] // build metadata must not affect precedence
|
||||
|
||||
// less than
|
||||
[TestCase("0.5.7", "0.5.8", ExpectedResult = true)]
|
||||
|
@ -192,6 +203,8 @@ namespace SMAPI.Tests.Utilities
|
|||
[TestCase("1.0-beta", "1.0-beta", ExpectedResult = false)]
|
||||
[TestCase("1.0-beta.10", "1.0-beta.10", ExpectedResult = false)]
|
||||
[TestCase("1.0-beta", "1.0-beta ", ExpectedResult = false)]
|
||||
[TestCase("1.0-beta+build.001", "1.0-beta+build.001", ExpectedResult = false)] // build metadata must not affect precedence
|
||||
[TestCase("1.0-beta+build.001", "1.0-beta+build.006", ExpectedResult = false)] // build metadata must not affect precedence
|
||||
|
||||
// less than
|
||||
[TestCase("0.5.7", "0.5.8", ExpectedResult = false)]
|
||||
|
@ -279,6 +292,8 @@ namespace SMAPI.Tests.Utilities
|
|||
[TestCase("1.11")]
|
||||
[TestCase("1.2")]
|
||||
[TestCase("1.2.15")]
|
||||
[TestCase("1.4.0.1")]
|
||||
[TestCase("1.4.0.6")]
|
||||
public void GameVersion(string versionStr)
|
||||
{
|
||||
// act
|
||||
|
@ -286,7 +301,6 @@ namespace SMAPI.Tests.Utilities
|
|||
|
||||
// assert
|
||||
Assert.AreEqual(versionStr, version.ToString(), "The game version did not round-trip to the same value.");
|
||||
Assert.IsTrue(version.IsOlderThan(new SemanticVersion("1.2.30")), "The game version should be considered older than the later semantic versions.");
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -20,6 +20,9 @@ namespace StardewModdingAPI
|
|||
/// <summary>An optional prerelease tag.</summary>
|
||||
string PrereleaseTag { get; }
|
||||
|
||||
/// <summary>Optional build metadata. This is ignored when determining version precedence.</summary>
|
||||
string BuildMetadata { get; }
|
||||
|
||||
|
||||
/*********
|
||||
** Accessors
|
||||
|
|
|
@ -7,8 +7,7 @@ namespace StardewModdingAPI.Toolkit
|
|||
/// <remarks>
|
||||
/// The implementation is defined by Semantic Version 2.0 (https://semver.org/), with a few deviations:
|
||||
/// - short-form "x.y" versions are supported (equivalent to "x.y.0");
|
||||
/// - hyphens are synonymous with dots in prerelease tags (like "-unofficial.3-pathoschild");
|
||||
/// - +build suffixes are not supported;
|
||||
/// - hyphens are synonymous with dots in prerelease tags and build metadata (like "-unofficial.3-pathoschild");
|
||||
/// - and "-unofficial" in prerelease tags is always lower-precedence (e.g. "1.0-beta" is newer than "1.0-unofficial").
|
||||
/// </remarks>
|
||||
public class SemanticVersion : ISemanticVersion
|
||||
|
@ -16,11 +15,11 @@ namespace StardewModdingAPI.Toolkit
|
|||
/*********
|
||||
** Fields
|
||||
*********/
|
||||
/// <summary>A regex pattern matching a valid prerelease tag.</summary>
|
||||
/// <summary>A regex pattern matching a valid prerelease or build metadata tag.</summary>
|
||||
internal const string TagPattern = @"(?>[a-z0-9]+[\-\.]?)+";
|
||||
|
||||
/// <summary>A regex pattern matching a version within a larger string.</summary>
|
||||
internal const string UnboundedVersionPattern = @"(?>(?<major>0|[1-9]\d*))\.(?>(?<minor>0|[1-9]\d*))(?>(?:\.(?<patch>0|[1-9]\d*))?)(?:-(?<prerelease>" + SemanticVersion.TagPattern + "))?";
|
||||
internal const string UnboundedVersionPattern = @"(?>(?<major>0|[1-9]\d*))\.(?>(?<minor>0|[1-9]\d*))(?>(?:\.(?<patch>0|[1-9]\d*))?)(?:-(?<prerelease>" + SemanticVersion.TagPattern + "))?(?:\\+(?<buildmetadata>" + SemanticVersion.TagPattern + "))?";
|
||||
|
||||
/// <summary>A regular expression matching a semantic version string.</summary>
|
||||
/// <remarks>This pattern is derived from the BNF documentation in the <a href="https://github.com/mojombo/semver">semver repo</a>, with deviations to support the Stardew Valley mod conventions (see remarks on <see cref="SemanticVersion"/>).</remarks>
|
||||
|
@ -42,6 +41,9 @@ namespace StardewModdingAPI.Toolkit
|
|||
/// <summary>An optional prerelease tag.</summary>
|
||||
public string PrereleaseTag { get; }
|
||||
|
||||
/// <summary>Optional build metadata. This is ignored when determining version precedence.</summary>
|
||||
public string BuildMetadata { get; }
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
|
@ -51,12 +53,14 @@ namespace StardewModdingAPI.Toolkit
|
|||
/// <param name="minor">The minor version incremented for backwards-compatible changes.</param>
|
||||
/// <param name="patch">The patch version for backwards-compatible fixes.</param>
|
||||
/// <param name="prereleaseTag">An optional prerelease tag.</param>
|
||||
public SemanticVersion(int major, int minor, int patch, string prereleaseTag = null)
|
||||
/// <param name="buildMetadata">Optional build metadata. This is ignored when determining version precedence.</param>
|
||||
public SemanticVersion(int major, int minor, int patch, string prereleaseTag = null, string buildMetadata = null)
|
||||
{
|
||||
this.MajorVersion = major;
|
||||
this.MinorVersion = minor;
|
||||
this.PatchVersion = patch;
|
||||
this.PrereleaseTag = this.GetNormalizedTag(prereleaseTag);
|
||||
this.BuildMetadata = this.GetNormalizedTag(buildMetadata);
|
||||
|
||||
this.AssertValid();
|
||||
}
|
||||
|
@ -94,6 +98,7 @@ namespace StardewModdingAPI.Toolkit
|
|||
this.MinorVersion = match.Groups["minor"].Success ? int.Parse(match.Groups["minor"].Value) : 0;
|
||||
this.PatchVersion = match.Groups["patch"].Success ? int.Parse(match.Groups["patch"].Value) : 0;
|
||||
this.PrereleaseTag = match.Groups["prerelease"].Success ? this.GetNormalizedTag(match.Groups["prerelease"].Value) : null;
|
||||
this.BuildMetadata = match.Groups["buildmetadata"].Success ? this.GetNormalizedTag(match.Groups["buildmetadata"].Value) : null;
|
||||
|
||||
this.AssertValid();
|
||||
}
|
||||
|
@ -175,6 +180,8 @@ namespace StardewModdingAPI.Toolkit
|
|||
string version = $"{this.MajorVersion}.{this.MinorVersion}.{this.PatchVersion}";
|
||||
if (this.PrereleaseTag != null)
|
||||
version += $"-{this.PrereleaseTag}";
|
||||
if (this.BuildMetadata != null)
|
||||
version += $"+{this.BuildMetadata}";
|
||||
return version;
|
||||
}
|
||||
|
||||
|
@ -200,7 +207,7 @@ namespace StardewModdingAPI.Toolkit
|
|||
/*********
|
||||
** Private methods
|
||||
*********/
|
||||
/// <summary>Get a normalized build tag.</summary>
|
||||
/// <summary>Get a normalized prerelease or build tag.</summary>
|
||||
/// <param name="tag">The tag to normalize.</param>
|
||||
private string GetNormalizedTag(string tag)
|
||||
{
|
||||
|
@ -277,12 +284,21 @@ namespace StardewModdingAPI.Toolkit
|
|||
throw new FormatException($"{this} isn't a valid semantic version. The major, minor, and patch numbers can't be negative.");
|
||||
if (this.MajorVersion == 0 && this.MinorVersion == 0 && this.PatchVersion == 0)
|
||||
throw new FormatException($"{this} isn't a valid semantic version. At least one of the major, minor, and patch numbers must be more than zero.");
|
||||
|
||||
if (this.PrereleaseTag != null)
|
||||
{
|
||||
if (this.PrereleaseTag.Trim() == "")
|
||||
throw new FormatException($"{this} isn't a valid semantic version. The tag cannot be a blank string (but may be omitted).");
|
||||
throw new FormatException($"{this} isn't a valid semantic version. The prerelease tag cannot be a blank string (but may be omitted).");
|
||||
if (!Regex.IsMatch(this.PrereleaseTag, $"^{SemanticVersion.TagPattern}$", RegexOptions.IgnoreCase))
|
||||
throw new FormatException($"{this} isn't a valid semantic version. The tag is invalid.");
|
||||
throw new FormatException($"{this} isn't a valid semantic version. The prerelease tag is invalid.");
|
||||
}
|
||||
|
||||
if (this.BuildMetadata != null)
|
||||
{
|
||||
if (this.BuildMetadata.Trim() == "")
|
||||
throw new FormatException($"{this} isn't a valid semantic version. The build metadata cannot be a blank string (but may be omitted).");
|
||||
if (!Regex.IsMatch(this.BuildMetadata, $"^{SemanticVersion.TagPattern}$", RegexOptions.IgnoreCase))
|
||||
throw new FormatException($"{this} isn't a valid semantic version. The build metadata is invalid.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Pathoschild.Http.Client;
|
||||
using StardewModdingAPI.Toolkit.Framework.Clients.WebApi;
|
||||
|
||||
namespace SMAPI.Web.LegacyRedirects.Controllers
|
||||
{
|
||||
/// <summary>Provides an API to perform mod update checks.</summary>
|
||||
[ApiController]
|
||||
[Produces("application/json")]
|
||||
[Route("api/v{version}/mods")]
|
||||
public class ModsApiController : Controller
|
||||
{
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Fetch version metadata for the given mods.</summary>
|
||||
/// <param name="model">The mod search criteria.</param>
|
||||
[HttpPost]
|
||||
public async Task<IEnumerable<ModEntryModel>> PostAsync([FromBody] ModSearchModel model)
|
||||
{
|
||||
using IClient client = new FluentClient("https://smapi.io/api");
|
||||
|
||||
Startup.ConfigureJsonNet(client.Formatters.JsonFormatter.SerializerSettings);
|
||||
|
||||
return await client
|
||||
.PostAsync(this.Request.Path)
|
||||
.WithBody(model)
|
||||
.AsArray<ModEntryModel>();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
using System;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Rewrite;
|
||||
|
||||
namespace SMAPI.Web.LegacyRedirects.Framework
|
||||
{
|
||||
/// <summary>Rewrite requests to prepend the subdomain portion (if any) to the path.</summary>
|
||||
/// <remarks>Derived from <a href="https://stackoverflow.com/a/44526747/262123" />.</remarks>
|
||||
internal class LambdaRewriteRule : IRule
|
||||
{
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>Rewrite an HTTP request if needed.</summary>
|
||||
private readonly Action<RewriteContext, HttpRequest, HttpResponse> Rewrite;
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="rewrite">Rewrite an HTTP request if needed.</param>
|
||||
public LambdaRewriteRule(Action<RewriteContext, HttpRequest, HttpResponse> rewrite)
|
||||
{
|
||||
this.Rewrite = rewrite ?? throw new ArgumentNullException(nameof(rewrite));
|
||||
}
|
||||
|
||||
/// <summary>Applies the rule. Implementations of ApplyRule should set the value for <see cref="RewriteContext.Result" /> (defaults to RuleResult.ContinueRules).</summary>
|
||||
/// <param name="context">The rewrite context.</param>
|
||||
public void ApplyRule(RewriteContext context)
|
||||
{
|
||||
HttpRequest request = context.HttpContext.Request;
|
||||
HttpResponse response = context.HttpContext.Response;
|
||||
this.Rewrite(context, request, response);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace SMAPI.Web.LegacyRedirects
|
||||
{
|
||||
/// <summary>The main app entry point.</summary>
|
||||
public class Program
|
||||
{
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>The main app entry point.</summary>
|
||||
/// <param name="args">The command-line arguments.</param>
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
Host
|
||||
.CreateDefaultBuilder(args)
|
||||
.ConfigureWebHostDefaults(builder => builder.UseStartup<Startup>())
|
||||
.Build()
|
||||
.Run();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:52756",
|
||||
"sslPort": 0
|
||||
}
|
||||
},
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"SMAPI.Web.LegacyRedirects": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "/",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"applicationUrl": "https://localhost:5001;http://localhost:5000"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Remove="aws-beanstalk-tools-defaults.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.0.0" />
|
||||
<PackageReference Include="Pathoschild.Http.FluentClient" Version="3.3.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SMAPI.Toolkit.CoreInterfaces\SMAPI.Toolkit.CoreInterfaces.csproj" />
|
||||
<ProjectReference Include="..\SMAPI.Toolkit\SMAPI.Toolkit.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,94 @@
|
|||
using System.Net;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Rewrite;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Newtonsoft.Json;
|
||||
using SMAPI.Web.LegacyRedirects.Framework;
|
||||
using StardewModdingAPI.Toolkit.Serialization;
|
||||
|
||||
namespace SMAPI.Web.LegacyRedirects
|
||||
{
|
||||
/// <summary>The web app startup configuration.</summary>
|
||||
public class Startup
|
||||
{
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>The method called by the runtime to add services to the container.</summary>
|
||||
/// <param name="services">The service injection container.</param>
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services
|
||||
.AddControllers()
|
||||
.AddNewtonsoftJson(options => Startup.ConfigureJsonNet(options.SerializerSettings));
|
||||
}
|
||||
|
||||
/// <summary>The method called by the runtime to configure the HTTP request pipeline.</summary>
|
||||
/// <param name="app">The application builder.</param>
|
||||
/// <param name="env">The hosting environment.</param>
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||
{
|
||||
if (env.IsDevelopment())
|
||||
app.UseDeveloperExceptionPage();
|
||||
|
||||
app
|
||||
.UseRewriter(this.GetRedirectRules())
|
||||
.UseRouting()
|
||||
.UseAuthorization()
|
||||
.UseEndpoints(endpoints => endpoints.MapControllers());
|
||||
}
|
||||
|
||||
/// <summary>Configure a Json.NET serializer.</summary>
|
||||
/// <param name="settings">The serializer settings to edit.</param>
|
||||
internal static void ConfigureJsonNet(JsonSerializerSettings settings)
|
||||
{
|
||||
foreach (JsonConverter converter in new JsonHelper().JsonSettings.Converters)
|
||||
settings.Converters.Add(converter);
|
||||
|
||||
settings.Formatting = Formatting.Indented;
|
||||
settings.NullValueHandling = NullValueHandling.Ignore;
|
||||
}
|
||||
|
||||
|
||||
/*********
|
||||
** Private methods
|
||||
*********/
|
||||
/// <summary>Get the redirect rules to apply.</summary>
|
||||
private RewriteOptions GetRedirectRules()
|
||||
{
|
||||
var redirects = new RewriteOptions();
|
||||
|
||||
redirects.Add(
|
||||
new LambdaRewriteRule((context, request, response) =>
|
||||
{
|
||||
string host = request.Host.Host;
|
||||
|
||||
// map API requests to proxy
|
||||
// This is needed because the low-level HTTP client SMAPI uses for Linux/Mac compatibility doesn't support redirects.
|
||||
if (host == "api.smapi.io")
|
||||
{
|
||||
request.Path = $"/api{request.Path}";
|
||||
return;
|
||||
}
|
||||
|
||||
// redirect other requests to Azure
|
||||
string newRoot = host switch
|
||||
{
|
||||
"api.smapi.io" => "smapi.io/api",
|
||||
"json.smapi.io" => "smapi.io/json",
|
||||
"log.smapi.io" => "smapi.io/log",
|
||||
"mods.smapi.io" => "smapi.io/mods",
|
||||
_ => "smapi.io"
|
||||
};
|
||||
response.StatusCode = (int)HttpStatusCode.PermanentRedirect;
|
||||
response.Headers["Location"] = $"{(request.IsHttps ? "https" : "http")}://{newRoot}{request.PathBase}{request.Path}{request.QueryString}";
|
||||
context.Result = RuleResult.EndResponse;
|
||||
})
|
||||
);
|
||||
|
||||
return redirects;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,14 +5,12 @@ using System.Linq;
|
|||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json.Schema;
|
||||
using StardewModdingAPI.Web.Framework;
|
||||
using StardewModdingAPI.Web.Framework.Clients.Pastebin;
|
||||
using StardewModdingAPI.Web.Framework.Compression;
|
||||
using StardewModdingAPI.Web.Framework.ConfigModels;
|
||||
using StardewModdingAPI.Web.ViewModels.JsonValidator;
|
||||
|
||||
namespace StardewModdingAPI.Web.Controllers
|
||||
|
@ -23,18 +21,12 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
/*********
|
||||
** Fields
|
||||
*********/
|
||||
/// <summary>The site config settings.</summary>
|
||||
private readonly SiteConfig Config;
|
||||
|
||||
/// <summary>The underlying Pastebin client.</summary>
|
||||
private readonly IPastebinClient Pastebin;
|
||||
|
||||
/// <summary>The underlying text compression helper.</summary>
|
||||
private readonly IGzipHelper GzipHelper;
|
||||
|
||||
/// <summary>The section URL for the schema validator.</summary>
|
||||
private string SectionUrl => this.Config.JsonValidatorUrl;
|
||||
|
||||
/// <summary>The supported JSON schemas (names indexed by ID).</summary>
|
||||
private readonly IDictionary<string, string> SchemaFormats = new Dictionary<string, string>
|
||||
{
|
||||
|
@ -57,12 +49,10 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
** Constructor
|
||||
***/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="siteConfig">The context config settings.</param>
|
||||
/// <param name="pastebin">The Pastebin API client.</param>
|
||||
/// <param name="gzipHelper">The underlying text compression helper.</param>
|
||||
public JsonValidatorController(IOptions<SiteConfig> siteConfig, IPastebinClient pastebin, IGzipHelper gzipHelper)
|
||||
public JsonValidatorController(IPastebinClient pastebin, IGzipHelper gzipHelper)
|
||||
{
|
||||
this.Config = siteConfig.Value;
|
||||
this.Pastebin = pastebin;
|
||||
this.GzipHelper = gzipHelper;
|
||||
}
|
||||
|
@ -81,7 +71,7 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
{
|
||||
schemaName = this.NormalizeSchemaName(schemaName);
|
||||
|
||||
var result = new JsonValidatorModel(this.SectionUrl, id, schemaName, this.SchemaFormats);
|
||||
var result = new JsonValidatorModel(id, schemaName, this.SchemaFormats);
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
return this.View("Index", result);
|
||||
|
||||
|
@ -142,7 +132,7 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
public async Task<ActionResult> PostAsync(JsonValidatorRequestModel request)
|
||||
{
|
||||
if (request == null)
|
||||
return this.View("Index", new JsonValidatorModel(this.SectionUrl, null, null, this.SchemaFormats).SetUploadError("The request seems to be invalid."));
|
||||
return this.View("Index", new JsonValidatorModel(null, null, this.SchemaFormats).SetUploadError("The request seems to be invalid."));
|
||||
|
||||
// normalize schema name
|
||||
string schemaName = this.NormalizeSchemaName(request.SchemaName);
|
||||
|
@ -150,7 +140,7 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
// get raw log text
|
||||
string input = request.Content;
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
return this.View("Index", new JsonValidatorModel(this.SectionUrl, null, schemaName, this.SchemaFormats).SetUploadError("The JSON file seems to be empty."));
|
||||
return this.View("Index", new JsonValidatorModel(null, schemaName, this.SchemaFormats).SetUploadError("The JSON file seems to be empty."));
|
||||
|
||||
// upload log
|
||||
input = this.GzipHelper.CompressString(input);
|
||||
|
@ -158,12 +148,10 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
|
||||
// handle errors
|
||||
if (!result.Success)
|
||||
return this.View("Index", new JsonValidatorModel(this.SectionUrl, result.ID, schemaName, this.SchemaFormats).SetUploadError($"Pastebin error: {result.Error ?? "unknown error"}"));
|
||||
return this.View("Index", new JsonValidatorModel(result.ID, schemaName, this.SchemaFormats).SetUploadError($"Pastebin error: {result.Error ?? "unknown error"}"));
|
||||
|
||||
// redirect to view
|
||||
UriBuilder uri = new UriBuilder(new Uri(this.SectionUrl));
|
||||
uri.Path = $"{uri.Path.TrimEnd('/')}/{schemaName}/{result.ID}";
|
||||
return this.Redirect(uri.Uri.ToString());
|
||||
return this.Redirect(this.Url.PlainAction("Index", "JsonValidator", new { schemaName = schemaName, id = result.ID }));
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Amazon;
|
||||
using Amazon.Runtime;
|
||||
using Amazon.S3;
|
||||
using Amazon.S3.Model;
|
||||
using Amazon.S3.Transfer;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StardewModdingAPI.Toolkit.Utilities;
|
||||
|
@ -20,8 +27,8 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
/*********
|
||||
** Fields
|
||||
*********/
|
||||
/// <summary>The site config settings.</summary>
|
||||
private readonly SiteConfig Config;
|
||||
/// <summary>The API client settings.</summary>
|
||||
private readonly ApiClientsConfig ClientsConfig;
|
||||
|
||||
/// <summary>The underlying Pastebin client.</summary>
|
||||
private readonly IPastebinClient Pastebin;
|
||||
|
@ -37,12 +44,12 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
** Constructor
|
||||
***/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="siteConfig">The context config settings.</param>
|
||||
/// <param name="clientsConfig">The API client settings.</param>
|
||||
/// <param name="pastebin">The Pastebin API client.</param>
|
||||
/// <param name="gzipHelper">The underlying text compression helper.</param>
|
||||
public LogParserController(IOptions<SiteConfig> siteConfig, IPastebinClient pastebin, IGzipHelper gzipHelper)
|
||||
public LogParserController(IOptions<ApiClientsConfig> clientsConfig, IPastebinClient pastebin, IGzipHelper gzipHelper)
|
||||
{
|
||||
this.Config = siteConfig.Value;
|
||||
this.ClientsConfig = clientsConfig.Value;
|
||||
this.Pastebin = pastebin;
|
||||
this.GzipHelper = gzipHelper;
|
||||
}
|
||||
|
@ -66,8 +73,9 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
PasteInfo paste = await this.GetAsync(id);
|
||||
ParsedLog log = paste.Success
|
||||
? new LogParser().Parse(paste.Content)
|
||||
: new ParsedLog { IsValid = false, Error = "Pastebin error: " + paste.Error };
|
||||
return this.View("Index", this.GetModel(id).SetResult(log, raw));
|
||||
: new ParsedLog { IsValid = false, Error = paste.Error };
|
||||
|
||||
return this.View("Index", this.GetModel(id, uploadWarning: paste.Warning, expiry: paste.Expiry).SetResult(log, raw));
|
||||
}
|
||||
|
||||
/***
|
||||
|
@ -85,16 +93,12 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
|
||||
// upload log
|
||||
input = this.GzipHelper.CompressString(input);
|
||||
SavePasteResult result = await this.Pastebin.PostAsync($"SMAPI log {DateTime.UtcNow:s}", input);
|
||||
|
||||
// handle errors
|
||||
if (!result.Success)
|
||||
return this.View("Index", this.GetModel(result.ID, uploadError: $"Pastebin error: {result.Error ?? "unknown error"}"));
|
||||
var uploadResult = await this.TrySaveLog(input);
|
||||
if (!uploadResult.Succeeded)
|
||||
return this.View("Index", this.GetModel(null, uploadError: uploadResult.UploadError));
|
||||
|
||||
// redirect to view
|
||||
UriBuilder uri = new UriBuilder(new Uri(this.Config.LogParserUrl));
|
||||
uri.Path = uri.Path.TrimEnd('/') + '/' + result.ID;
|
||||
return this.Redirect(uri.Uri.ToString());
|
||||
return this.Redirect(this.Url.PlainAction("Index", "LogParser", new { id = uploadResult.ID }));
|
||||
}
|
||||
|
||||
|
||||
|
@ -105,19 +109,115 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
/// <param name="id">The Pastebin paste ID.</param>
|
||||
private async Task<PasteInfo> GetAsync(string id)
|
||||
{
|
||||
PasteInfo response = await this.Pastebin.GetAsync(id);
|
||||
response.Content = this.GzipHelper.DecompressString(response.Content);
|
||||
return response;
|
||||
// get from Amazon S3
|
||||
if (Guid.TryParseExact(id, "N", out Guid _))
|
||||
{
|
||||
var credentials = new BasicAWSCredentials(accessKey: this.ClientsConfig.AmazonAccessKey, secretKey: this.ClientsConfig.AmazonSecretKey);
|
||||
|
||||
using (IAmazonS3 s3 = new AmazonS3Client(credentials, RegionEndpoint.GetBySystemName(this.ClientsConfig.AmazonRegion)))
|
||||
{
|
||||
try
|
||||
{
|
||||
using (GetObjectResponse response = await s3.GetObjectAsync(this.ClientsConfig.AmazonLogBucket, $"logs/{id}"))
|
||||
using (Stream responseStream = response.ResponseStream)
|
||||
using (StreamReader reader = new StreamReader(responseStream))
|
||||
{
|
||||
DateTime expiry = response.Expiration.ExpiryDateUtc;
|
||||
string pastebinError = response.Metadata["x-amz-meta-pastebin-error"];
|
||||
string content = this.GzipHelper.DecompressString(reader.ReadToEnd());
|
||||
|
||||
return new PasteInfo
|
||||
{
|
||||
Success = true,
|
||||
Content = content,
|
||||
Expiry = expiry,
|
||||
Warning = pastebinError
|
||||
};
|
||||
}
|
||||
}
|
||||
catch (AmazonServiceException ex)
|
||||
{
|
||||
return ex.ErrorCode == "NoSuchKey"
|
||||
? new PasteInfo { Error = "There's no log with that ID." }
|
||||
: new PasteInfo { Error = $"Could not fetch that log from AWS S3 ({ex.ErrorCode}: {ex.Message})." };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// get from PasteBin
|
||||
else
|
||||
{
|
||||
PasteInfo response = await this.Pastebin.GetAsync(id);
|
||||
response.Content = this.GzipHelper.DecompressString(response.Content);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Save a log to Pastebin or Amazon S3, if available.</summary>
|
||||
/// <param name="content">The content to upload.</param>
|
||||
/// <returns>Returns metadata about the save attempt.</returns>
|
||||
private async Task<UploadResult> TrySaveLog(string content)
|
||||
{
|
||||
// save to PasteBin
|
||||
string uploadError;
|
||||
{
|
||||
SavePasteResult result = await this.Pastebin.PostAsync($"SMAPI log {DateTime.UtcNow:s}", content);
|
||||
if (result.Success)
|
||||
return new UploadResult(true, result.ID, null);
|
||||
|
||||
uploadError = $"Pastebin error: {result.Error ?? "unknown error"}";
|
||||
}
|
||||
|
||||
// fallback to S3
|
||||
try
|
||||
{
|
||||
var credentials = new BasicAWSCredentials(accessKey: this.ClientsConfig.AmazonAccessKey, secretKey: this.ClientsConfig.AmazonSecretKey);
|
||||
|
||||
using (Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(content)))
|
||||
using (IAmazonS3 s3 = new AmazonS3Client(credentials, RegionEndpoint.GetBySystemName(this.ClientsConfig.AmazonRegion)))
|
||||
using (TransferUtility uploader = new TransferUtility(s3))
|
||||
{
|
||||
string id = Guid.NewGuid().ToString("N");
|
||||
|
||||
var uploadRequest = new TransferUtilityUploadRequest
|
||||
{
|
||||
BucketName = this.ClientsConfig.AmazonLogBucket,
|
||||
Key = $"logs/{id}",
|
||||
InputStream = stream,
|
||||
Metadata =
|
||||
{
|
||||
// note: AWS will lowercase keys and prefix 'x-amz-meta-'
|
||||
["smapi-uploaded"] = DateTime.UtcNow.ToString("O"),
|
||||
["pastebin-error"] = uploadError
|
||||
}
|
||||
};
|
||||
|
||||
await uploader.UploadAsync(uploadRequest);
|
||||
|
||||
return new UploadResult(true, id, uploadError);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new UploadResult(false, null, $"{uploadError}\n{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Build a log parser model.</summary>
|
||||
/// <param name="pasteID">The paste ID.</param>
|
||||
/// <param name="uploadError">An error which occurred while uploading the log to Pastebin.</param>
|
||||
private LogParserModel GetModel(string pasteID, string uploadError = null)
|
||||
/// <param name="expiry">When the uploaded file will no longer be available.</param>
|
||||
/// <param name="uploadWarning">A non-blocking warning while uploading the log.</param>
|
||||
/// <param name="uploadError">An error which occurred while uploading the log.</param>
|
||||
private LogParserModel GetModel(string pasteID, DateTime? expiry = null, string uploadWarning = null, string uploadError = null)
|
||||
{
|
||||
string sectionUrl = this.Config.LogParserUrl;
|
||||
Platform? platform = this.DetectClientPlatform();
|
||||
return new LogParserModel(sectionUrl, pasteID, platform) { UploadError = uploadError };
|
||||
|
||||
return new LogParserModel(pasteID, platform)
|
||||
{
|
||||
UploadWarning = uploadWarning,
|
||||
UploadError = uploadError,
|
||||
Expiry = expiry
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Detect the viewer's OS.</summary>
|
||||
|
@ -143,5 +243,36 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The result of an attempt to upload a file.</summary>
|
||||
private class UploadResult
|
||||
{
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>Whether the file upload succeeded.</summary>
|
||||
public bool Succeeded { get; }
|
||||
|
||||
/// <summary>The file ID, if applicable.</summary>
|
||||
public string ID { get; }
|
||||
|
||||
/// <summary>The upload error, if any.</summary>
|
||||
public string UploadError { get; }
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="succeeded">Whether the file upload succeeded.</param>
|
||||
/// <param name="id">The file ID, if applicable.</param>
|
||||
/// <param name="uploadError">The upload error, if any.</param>
|
||||
public UploadResult(bool succeeded, string id, string uploadError)
|
||||
{
|
||||
this.Succeeded = succeeded;
|
||||
this.ID = id;
|
||||
this.UploadError = uploadError;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ using StardewModdingAPI.Toolkit.Framework.Clients.WebApi;
|
|||
using StardewModdingAPI.Toolkit.Framework.Clients.Wiki;
|
||||
using StardewModdingAPI.Toolkit.Framework.ModData;
|
||||
using StardewModdingAPI.Toolkit.Framework.UpdateData;
|
||||
using StardewModdingAPI.Web.Framework;
|
||||
using StardewModdingAPI.Web.Framework.Caching.Mods;
|
||||
using StardewModdingAPI.Web.Framework.Caching.Wiki;
|
||||
using StardewModdingAPI.Web.Framework.Clients.Chucklefish;
|
||||
|
@ -49,9 +50,6 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
/// <summary>The internal mod metadata list.</summary>
|
||||
private readonly ModDatabase ModDatabase;
|
||||
|
||||
/// <summary>The web URL for the compatibility list.</summary>
|
||||
private readonly string CompatibilityPageUrl;
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
|
@ -70,7 +68,6 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
{
|
||||
this.ModDatabase = new ModToolkit().GetModDatabase(Path.Combine(environment.WebRootPath, "SMAPI.metadata.json"));
|
||||
ModUpdateCheckConfig config = configProvider.Value;
|
||||
this.CompatibilityPageUrl = config.CompatibilityPageUrl;
|
||||
|
||||
this.WikiCache = wikiCache;
|
||||
this.ModCache = modCache;
|
||||
|
@ -205,7 +202,7 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
|
||||
// get unofficial version
|
||||
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}");
|
||||
unofficial = new ModEntryVersionModel(wikiEntry.Compatibility.UnofficialVersion, $"{this.Url.PlainAction("Index", "Mods")}#{wikiEntry.Anchor}");
|
||||
|
||||
// get unofficial version for beta
|
||||
if (wikiEntry?.HasBetaInfo == true)
|
||||
|
@ -215,7 +212,7 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
if (wikiEntry.BetaCompatibility.UnofficialVersion != null)
|
||||
{
|
||||
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}")
|
||||
? new ModEntryVersionModel(wikiEntry.BetaCompatibility.UnofficialVersion, $"{this.Url.PlainAction("Index", "Mods")}#{wikiEntry.Anchor}")
|
||||
: null;
|
||||
}
|
||||
else
|
||||
|
|
|
@ -1,54 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework
|
||||
{
|
||||
/// <summary>Reads configuration values from the AWS Beanstalk environment properties file (if present).</summary>
|
||||
/// <remarks>This is a workaround for AWS Beanstalk injection not working with .NET Core apps.</remarks>
|
||||
internal class BeanstalkEnvPropsConfigProvider : ConfigurationProvider, IConfigurationSource
|
||||
{
|
||||
/*********
|
||||
** Fields
|
||||
*********/
|
||||
/// <summary>The absolute path to the container configuration file on an Amazon EC2 instance.</summary>
|
||||
private const string ContainerConfigPath = @"C:\Program Files\Amazon\ElasticBeanstalk\config\containerconfiguration";
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Build the configuration provider for this source.</summary>
|
||||
/// <param name="builder">The configuration builder.</param>
|
||||
public IConfigurationProvider Build(IConfigurationBuilder builder)
|
||||
{
|
||||
return new BeanstalkEnvPropsConfigProvider();
|
||||
}
|
||||
|
||||
/// <summary>Load the environment properties.</summary>
|
||||
public override void Load()
|
||||
{
|
||||
this.Data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// get Beanstalk config file
|
||||
FileInfo file = new FileInfo(BeanstalkEnvPropsConfigProvider.ContainerConfigPath);
|
||||
if (!file.Exists)
|
||||
return;
|
||||
|
||||
// parse JSON
|
||||
JObject jsonRoot = (JObject)JsonConvert.DeserializeObject(File.ReadAllText(file.FullName));
|
||||
if (jsonRoot["iis"]?["env"] is JArray jsonProps)
|
||||
{
|
||||
foreach (string prop in jsonProps.Values<string>())
|
||||
{
|
||||
string[] parts = prop.Split('=', 2); // key=value
|
||||
if (parts.Length == 2)
|
||||
this.Data[parts[0]] = parts[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
using System;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.Clients.Pastebin
|
||||
{
|
||||
/// <summary>The response for a get-paste request.</summary>
|
||||
|
@ -9,7 +11,13 @@ namespace StardewModdingAPI.Web.Framework.Clients.Pastebin
|
|||
/// <summary>The fetched paste content (if <see cref="Success"/> is <c>true</c>).</summary>
|
||||
public string Content { get; set; }
|
||||
|
||||
/// <summary>The error message (if saving failed).</summary>
|
||||
/// <summary>When the file will no longer be available.</summary>
|
||||
public DateTime? Expiry { get; set; }
|
||||
|
||||
/// <summary>The error message if saving succeeded, but a non-blocking issue was encountered.</summary>
|
||||
public string Warning { get; set; }
|
||||
|
||||
/// <summary>The error message if saving failed.</summary>
|
||||
public string Error { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,7 +62,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.Pastebin
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new PasteInfo { Error = ex.ToString() };
|
||||
return new PasteInfo { Error = $"Pastebin error: {ex}" };
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,22 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels
|
|||
public string UserAgent { get; set; }
|
||||
|
||||
|
||||
/****
|
||||
** Amazon Web Services
|
||||
****/
|
||||
/// <summary>The access key for AWS authentication.</summary>
|
||||
public string AmazonAccessKey { get; set; }
|
||||
|
||||
/// <summary>The secret key for AWS authentication.</summary>
|
||||
public string AmazonSecretKey { get; set; }
|
||||
|
||||
/// <summary>The AWS region endpoint (like 'us-east-1').</summary>
|
||||
public string AmazonRegion { get; set; }
|
||||
|
||||
/// <summary>The AWS bucket in which to store temporary uploaded logs.</summary>
|
||||
public string AmazonLogBucket { get; set; }
|
||||
|
||||
|
||||
/****
|
||||
** Chucklefish
|
||||
****/
|
||||
|
|
|
@ -11,8 +11,5 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels
|
|||
|
||||
/// <summary>The number of minutes failed update checks should be cached before refetching them.</summary>
|
||||
public int ErrorCacheMinutes { get; set; }
|
||||
|
||||
/// <summary>The web URL for the wiki compatibility list.</summary>
|
||||
public string CompatibilityPageUrl { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,18 +6,6 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels
|
|||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The root URL for the app.</summary>
|
||||
public string RootUrl { get; set; }
|
||||
|
||||
/// <summary>The root URL for the log parser.</summary>
|
||||
public string LogParserUrl { get; set; }
|
||||
|
||||
/// <summary>The root URL for the JSON validator.</summary>
|
||||
public string JsonValidatorUrl { get; set; }
|
||||
|
||||
/// <summary>The root URL for the mod list.</summary>
|
||||
public string ModListUrl { get; set; }
|
||||
|
||||
/// <summary>Whether to show SMAPI beta versions on the main page, if any.</summary>
|
||||
public bool BetaEnabled { get; set; }
|
||||
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
using JetBrains.Annotations;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework
|
||||
{
|
||||
/// <summary>Provides extensions on ASP.NET Core types.</summary>
|
||||
public static class Extensions
|
||||
{
|
||||
/// <summary>Get a URL with the absolute path for an action method. Unlike <see cref="IUrlHelper.Action"/>, only the specified <paramref name="values"/> are added to the URL without merging values from the current HTTP request.</summary>
|
||||
/// <param name="helper">The URL helper to extend.</param>
|
||||
/// <param name="action">The name of the action method.</param>
|
||||
/// <param name="controller">The name of the controller.</param>
|
||||
/// <param name="values">An object that contains route values.</param>
|
||||
/// <returns>The generated URL.</returns>
|
||||
public static string PlainAction(this IUrlHelper helper, [AspMvcAction] string action, [AspMvcController] string controller, object values = null)
|
||||
{
|
||||
RouteValueDictionary valuesDict = new RouteValueDictionary(values);
|
||||
foreach (var value in helper.ActionContext.RouteData.Values)
|
||||
{
|
||||
if (!valuesDict.ContainsKey(value.Key))
|
||||
valuesDict[value.Key] = null; // explicitly remove it from the URL
|
||||
}
|
||||
|
||||
return helper.Action(action, controller, valuesDict);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
using System;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Rewrite;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.RewriteRules
|
||||
{
|
||||
/// <summary>Rewrite requests to prepend the subdomain portion (if any) to the path.</summary>
|
||||
/// <remarks>Derived from <a href="https://stackoverflow.com/a/44526747/262123" />.</remarks>
|
||||
internal class ConditionalRewriteSubdomainRule : IRule
|
||||
{
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>A predicate which indicates when the rule should be applied.</summary>
|
||||
private readonly Func<HttpRequest, bool> ShouldRewrite;
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="shouldRewrite">A predicate which indicates when the rule should be applied.</param>
|
||||
public ConditionalRewriteSubdomainRule(Func<HttpRequest, bool> shouldRewrite = null)
|
||||
{
|
||||
this.ShouldRewrite = shouldRewrite ?? (req => true);
|
||||
}
|
||||
|
||||
/// <summary>Applies the rule. Implementations of ApplyRule should set the value for <see cref="RewriteContext.Result" /> (defaults to RuleResult.ContinueRules).</summary>
|
||||
/// <param name="context">The rewrite context.</param>
|
||||
public void ApplyRule(RewriteContext context)
|
||||
{
|
||||
HttpRequest request = context.HttpContext.Request;
|
||||
|
||||
// check condition
|
||||
if (!this.ShouldRewrite(request))
|
||||
return;
|
||||
|
||||
// get host parts
|
||||
string host = request.Host.Host;
|
||||
string[] parts = host.Split('.');
|
||||
if (parts.Length < 2)
|
||||
return;
|
||||
|
||||
// prepend to path
|
||||
request.Path = $"/{parts[0]}{request.Path}";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -12,16 +12,17 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AWSSDK.S3" Version="3.3.108.4" />
|
||||
<PackageReference Include="Hangfire.AspNetCore" Version="1.7.7" />
|
||||
<PackageReference Include="Hangfire.Mongo" Version="0.6.5" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.16" />
|
||||
<PackageReference Include="Humanizer.Core" Version="2.7.9" />
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2019.1.3" />
|
||||
<PackageReference Include="Markdig" Version="0.18.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Rewrite" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="3.0.1" />
|
||||
<PackageReference Include="MongoDB.Driver" Version="2.9.3" />
|
||||
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.11" />
|
||||
<PackageReference Include="Pathoschild.FluentNexus" Version="0.8.0" />
|
||||
|
|
|
@ -48,7 +48,7 @@ namespace StardewModdingAPI.Web
|
|||
.SetBasePath(env.ContentRootPath)
|
||||
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
|
||||
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
|
||||
.Add(new BeanstalkEnvPropsConfigProvider())
|
||||
.AddEnvironmentVariables()
|
||||
.Build();
|
||||
}
|
||||
|
||||
|
@ -58,6 +58,7 @@ namespace StardewModdingAPI.Web
|
|||
{
|
||||
// init basic services
|
||||
services
|
||||
.Configure<ApiClientsConfig>(this.Configuration.GetSection("ApiClients"))
|
||||
.Configure<BackgroundServicesConfig>(this.Configuration.GetSection("BackgroundServices"))
|
||||
.Configure<ModCompatibilityListConfig>(this.Configuration.GetSection("ModCompatibilityList"))
|
||||
.Configure<ModUpdateCheckConfig>(this.Configuration.GetSection("ModUpdateCheck"))
|
||||
|
@ -172,8 +173,7 @@ namespace StardewModdingAPI.Web
|
|||
.UseCors(policy => policy
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod()
|
||||
.WithOrigins("https://smapi.io", "https://*.smapi.io", "https://*.edge.smapi.io")
|
||||
.SetIsOriginAllowedToAllowWildcardSubdomains()
|
||||
.WithOrigins("https://smapi.io")
|
||||
)
|
||||
.UseRewriter(this.GetRedirectRules())
|
||||
.UseStaticFiles() // wwwroot folder
|
||||
|
@ -201,23 +201,13 @@ namespace StardewModdingAPI.Web
|
|||
shouldRewrite: req =>
|
||||
req.Host.Host != "localhost"
|
||||
&& !req.Path.StartsWithSegments("/api")
|
||||
&& !req.Host.Host.StartsWith("api.")
|
||||
));
|
||||
|
||||
// convert subdomain.smapi.io => smapi.io/subdomain for routing
|
||||
redirects.Add(new ConditionalRewriteSubdomainRule(
|
||||
shouldRewrite: req =>
|
||||
req.Host.Host != "localhost"
|
||||
&& (req.Host.Host.StartsWith("api.") || req.Host.Host.StartsWith("json.") || req.Host.Host.StartsWith("log.") || req.Host.Host.StartsWith("mods."))
|
||||
&& !req.Path.StartsWithSegments("/content")
|
||||
&& !req.Path.StartsWithSegments("/favicon.ico")
|
||||
));
|
||||
|
||||
// shortcut redirects
|
||||
redirects.Add(new RedirectToUrlRule(@"^/3\.0\.?$", "https://stardewvalleywiki.com/Modding:Migrate_to_SMAPI_3.0"));
|
||||
redirects.Add(new RedirectToUrlRule(@"^/(?:buildmsg|package)(?:/?(.*))$", "https://github.com/Pathoschild/SMAPI/blob/develop/docs/technical/mod-package.md#$1")); // buildmsg deprecated, remove when SDV 1.4 is released
|
||||
redirects.Add(new RedirectToUrlRule(@"^/community\.?$", "https://stardewvalleywiki.com/Modding:Community"));
|
||||
redirects.Add(new RedirectToUrlRule(@"^/compat\.?$", "https://mods.smapi.io"));
|
||||
redirects.Add(new RedirectToUrlRule(@"^/compat\.?$", "https://smapi.io/mods"));
|
||||
redirects.Add(new RedirectToUrlRule(@"^/docs\.?$", "https://stardewvalleywiki.com/Modding:Index"));
|
||||
redirects.Add(new RedirectToUrlRule(@"^/install\.?$", "https://stardewvalleywiki.com/Modding:Player_Guide/Getting_Started#Install_SMAPI"));
|
||||
redirects.Add(new RedirectToUrlRule(@"^/troubleshoot(.*)$", "https://stardewvalleywiki.com/Modding:Player_Guide/Troubleshooting$1"));
|
||||
|
|
|
@ -9,9 +9,6 @@ namespace StardewModdingAPI.Web.ViewModels.JsonValidator
|
|||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The root URL for the log parser controller.</summary>
|
||||
public string SectionUrl { get; set; }
|
||||
|
||||
/// <summary>The paste ID.</summary>
|
||||
public string PasteID { get; set; }
|
||||
|
||||
|
@ -44,13 +41,11 @@ namespace StardewModdingAPI.Web.ViewModels.JsonValidator
|
|||
public JsonValidatorModel() { }
|
||||
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="sectionUrl">The root URL for the log parser controller.</param>
|
||||
/// <param name="pasteID">The paste ID.</param>
|
||||
/// <param name="schemaName">The schema name with which the JSON was validated.</param>
|
||||
/// <param name="schemaFormats">The supported JSON schemas (names indexed by ID).</param>
|
||||
public JsonValidatorModel(string sectionUrl, string pasteID, string schemaName, IDictionary<string, string> schemaFormats)
|
||||
public JsonValidatorModel(string pasteID, string schemaName, IDictionary<string, string> schemaFormats)
|
||||
{
|
||||
this.SectionUrl = sectionUrl;
|
||||
this.PasteID = pasteID;
|
||||
this.SchemaName = schemaName;
|
||||
this.SchemaFormats = schemaFormats;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
@ -19,9 +20,6 @@ namespace StardewModdingAPI.Web.ViewModels
|
|||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The root URL for the log parser controller.</summary>
|
||||
public string SectionUrl { get; set; }
|
||||
|
||||
/// <summary>The paste ID.</summary>
|
||||
public string PasteID { get; set; }
|
||||
|
||||
|
@ -34,12 +32,18 @@ namespace StardewModdingAPI.Web.ViewModels
|
|||
/// <summary>Whether to show the raw unparsed log.</summary>
|
||||
public bool ShowRaw { get; set; }
|
||||
|
||||
/// <summary>An error which occurred while uploading the log to Pastebin.</summary>
|
||||
/// <summary>A non-blocking warning while uploading the log.</summary>
|
||||
public string UploadWarning { get; set; }
|
||||
|
||||
/// <summary>An error which occurred while uploading the log.</summary>
|
||||
public string UploadError { get; set; }
|
||||
|
||||
/// <summary>An error which occurred while parsing the log file.</summary>
|
||||
public string ParseError => this.ParsedLog?.Error;
|
||||
|
||||
/// <summary>When the uploaded file will no longer be available.</summary>
|
||||
public DateTime? Expiry { get; set; }
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
|
@ -48,12 +52,10 @@ namespace StardewModdingAPI.Web.ViewModels
|
|||
public LogParserModel() { }
|
||||
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="sectionUrl">The root URL for the log parser controller.</param>
|
||||
/// <param name="pasteID">The paste ID.</param>
|
||||
/// <param name="platform">The viewer's detected OS, if known.</param>
|
||||
public LogParserModel(string sectionUrl, string pasteID, Platform? platform)
|
||||
public LogParserModel(string pasteID, Platform? platform)
|
||||
{
|
||||
this.SectionUrl = sectionUrl;
|
||||
this.PasteID = pasteID;
|
||||
this.DetectedPlatform = platform;
|
||||
this.ParsedLog = null;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
@using Microsoft.Extensions.Options
|
||||
@using StardewModdingAPI.Web.Framework
|
||||
@using StardewModdingAPI.Web.Framework.ConfigModels
|
||||
@inject IOptions<SiteConfig> SiteConfig
|
||||
@model StardewModdingAPI.Web.ViewModels.IndexModel
|
||||
|
@ -44,14 +45,14 @@
|
|||
}
|
||||
<div><a href="https://stardewvalleywiki.com/Modding:Player_Guide" class="secondary-cta">Player guide</a></div>
|
||||
<div class="sublinks">
|
||||
<a href="https://github.com/Pathoschild/SMAPI">source code</a> | <a href="@(new UriBuilder(SiteConfig.Value.RootUrl) { Path = "privacy" }.Uri)">privacy</a>
|
||||
<a href="https://github.com/Pathoschild/SMAPI">source code</a> | <a href="@Url.PlainAction("Privacy", "Index")">privacy</a>
|
||||
</div>
|
||||
<img id="pufferchick" src="Content/images/pufferchick.png" />
|
||||
</div>
|
||||
|
||||
<h2 id="help">Get help</h2>
|
||||
<ul>
|
||||
<li><a href="@SiteConfig.Value.ModListUrl">Mod compatibility list</a></li>
|
||||
<li><a href="@Url.PlainAction("Index", "Mods")">Mod compatibility list</a></li>
|
||||
<li>Get help <a href="https://smapi.io/community">on Discord or in the forums</a></li>
|
||||
</ul>
|
||||
|
||||
|
@ -61,7 +62,7 @@
|
|||
<div class="github-description">
|
||||
@Html.Raw(Markdig.Markdown.ToHtml(Model.StableVersion.Description))
|
||||
</div>
|
||||
<p>See the <a href="https://github.com/Pathoschild/SMAPI/blob/develop/docs/release-notes.md#release-notes">release notes</a> and <a href="@SiteConfig.Value.ModListUrl">mod compatibility list</a> for more info.</p>
|
||||
<p>See the <a href="https://github.com/Pathoschild/SMAPI/blob/develop/docs/release-notes.md#release-notes">release notes</a> and <a href="@Url.PlainAction("Index", "Mods")">mod compatibility list</a> for more info.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -70,13 +71,13 @@ else
|
|||
<div class="github-description">
|
||||
@Html.Raw(Markdig.Markdown.ToHtml(Model.StableVersion.Description))
|
||||
</div>
|
||||
<p>See the <a href="https://github.com/Pathoschild/SMAPI/blob/develop/docs/release-notes.md#release-notes">release notes</a> and <a href="@SiteConfig.Value.ModListUrl">mod compatibility list</a> for more info.</p>
|
||||
<p>See the <a href="https://github.com/Pathoschild/SMAPI/blob/develop/docs/release-notes.md#release-notes">release notes</a> and <a href="@Url.PlainAction("Index", "Mods")">mod compatibility list</a> for more info.</p>
|
||||
|
||||
<h3>SMAPI @Model.BetaVersion.Version?</h3>
|
||||
<div class="github-description">
|
||||
@Html.Raw(Markdig.Markdown.ToHtml(Model.BetaVersion.Description))
|
||||
</div>
|
||||
<p>See the <a href="https://github.com/Pathoschild/SMAPI/blob/develop/docs/release-notes.md#release-notes">release notes</a> and <a href="@SiteConfig.Value.ModListUrl">mod compatibility list</a> for more info.</p>
|
||||
<p>See the <a href="https://github.com/Pathoschild/SMAPI/blob/develop/docs/release-notes.md#release-notes">release notes</a> and <a href="@Url.PlainAction("Index", "Mods")">mod compatibility list</a> for more info.</p>
|
||||
}
|
||||
|
||||
<h2 id="donate">Support SMAPI ♥</h2>
|
||||
|
@ -105,10 +106,12 @@ else
|
|||
|
||||
<p>
|
||||
Special thanks to
|
||||
<a href="https://www.nexusmods.com/users/65566526?tab=user+files">bwdy</a>,
|
||||
hawkfalcon,
|
||||
<a href="https://twitter.com/iKeychain">iKeychain</a>,
|
||||
jwdred,
|
||||
<a href="https://www.nexusmods.com/users/12252523">Karmylla</a>,
|
||||
<a href="https://www.nexusmods.com/stardewvalley/users/51777556">minervamaga</a>,
|
||||
Pucklynn,
|
||||
Renorien,
|
||||
Robby LaFarge,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
@using Microsoft.Extensions.Options
|
||||
@using StardewModdingAPI.Web.Framework
|
||||
@using StardewModdingAPI.Web.Framework.ConfigModels
|
||||
@inject IOptions<SiteConfig> SiteConfig
|
||||
@{
|
||||
|
@ -8,7 +9,7 @@
|
|||
<link rel="stylesheet" href="~/Content/css/privacy.css" />
|
||||
}
|
||||
|
||||
← <a href="@SiteConfig.Value.RootUrl">back to SMAPI page</a>
|
||||
← <a href="@Url.PlainAction("Index", "Index")">back to SMAPI page</a>
|
||||
|
||||
<p>SMAPI is an <a href="https://github.com/Pathoschild/SMAPI">open-source</a> and non-profit project. Your privacy is important, so this page explains what information SMAPI uses and transmits. <strong>This page is informational only, it's not a legal document.</strong></p>
|
||||
|
||||
|
@ -34,7 +35,7 @@
|
|||
</ol>
|
||||
|
||||
<h3>Log parser</h3>
|
||||
<p>The <a href="https://log.smapi.io/">log parser page</a> lets you store a log file for analysis and sharing. The log data is stored indefinitely in an obfuscated form as unlisted pastes in <a href="https://pastebin.com/">Pastebin</a>. No personal information is stored by the log parser beyond what you choose to upload, but see <em><a href="#web-logging">web logging</a></em> and the <a href="https://pastebin.com/doc_privacy_statement">Pastebin Privacy Statement</a>.</p>
|
||||
<p>The <a href="https://smapi.io/log">log parser page</a> lets you store a log file for analysis and sharing. The log data is stored indefinitely in an obfuscated form as unlisted pastes in <a href="https://pastebin.com/">Pastebin</a>. No personal information is stored by the log parser beyond what you choose to upload, but see <em><a href="#web-logging">web logging</a></em> and the <a href="https://pastebin.com/doc_privacy_statement">Pastebin Privacy Statement</a>.</p>
|
||||
|
||||
<h3>Multiplayer sync</h3>
|
||||
<p>As part of its multiplayer API, SMAPI transmits basic context to players you connect to (mainly your OS, SMAPI version, game version, and installed mods). This is used to enable multiplayer features like inter-mod messages, compatibility checks, etc. Although this information is normally hidden from players, it may be visible due to mods or configuration changes.</p>
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
@using StardewModdingAPI.Web.Framework
|
||||
@using StardewModdingAPI.Web.ViewModels.JsonValidator
|
||||
@model JsonValidatorModel
|
||||
|
||||
@{
|
||||
// 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 curPageUrl = this.Url.PlainAction("Index", "JsonValidator", new { schemaName = Model.SchemaName, id = Model.PasteID });
|
||||
string newUploadUrl = this.Url.PlainAction("Index", "JsonValidator", new { schemaName = Model.SchemaName });
|
||||
string schemaDisplayName = null;
|
||||
bool isValidSchema = Model.SchemaName != null && Model.SchemaFormats.TryGetValue(Model.SchemaName, out schemaDisplayName) && schemaDisplayName != "None";
|
||||
|
||||
|
@ -34,8 +35,8 @@
|
|||
<script src="https://cdn.jsdelivr.net/gh/tmont/sunlight@1.22.0/src/lang/sunlight.javascript.min.js" crossorigin="anonymous"></script>
|
||||
<script src="~/Content/js/json-validator.js"></script>
|
||||
<script>
|
||||
$(function () {
|
||||
smapi.jsonValidator(@Json.Serialize(Model.SectionUrl), @Json.Serialize(Model.PasteID));
|
||||
$(function() {
|
||||
smapi.jsonValidator(@Json.Serialize(this.Url.PlainAction("Index", "JsonValidator", values: null)), @Json.Serialize(Model.PasteID));
|
||||
});
|
||||
</script>
|
||||
}
|
||||
|
@ -70,7 +71,7 @@ else if (Model.PasteID != null)
|
|||
@if (Model.Content == null)
|
||||
{
|
||||
<h2>Upload a JSON file</h2>
|
||||
<form action="@Model.SectionUrl" method="post">
|
||||
<form action="@this.Url.PlainAction("PostAsync", "JsonValidator")" method="post">
|
||||
<ol>
|
||||
<li>
|
||||
Choose the JSON format:<br />
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
@using Humanizer
|
||||
@using Newtonsoft.Json
|
||||
@using StardewModdingAPI.Toolkit.Utilities
|
||||
@using StardewModdingAPI.Web.Framework
|
||||
@using StardewModdingAPI.Web.Framework.LogParsing.Models
|
||||
@model StardewModdingAPI.Web.ViewModels.LogParserModel
|
||||
|
||||
|
@ -18,7 +20,7 @@
|
|||
{
|
||||
<meta name="robots" content="noindex" />
|
||||
}
|
||||
<link rel="stylesheet" href="~/Content/css/log-parser.css?r=20190515" />
|
||||
<link rel="stylesheet" href="~/Content/css/log-parser.css?r=20191127" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.min.js" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.3.1/dist/jquery.min.js" crossorigin="anonymous"></script>
|
||||
<script src="~/Content/js/log-parser.js?r=20190515"></script>
|
||||
|
@ -31,7 +33,7 @@
|
|||
showSections: @Json.Serialize(Enum.GetNames(typeof(LogSection)).ToDictionary(section => section, section => false), noFormatting),
|
||||
showLevels: @Json.Serialize(defaultFilters, noFormatting),
|
||||
enableFilters: @Json.Serialize(!Model.ShowRaw)
|
||||
}, '@Model.SectionUrl');
|
||||
}, '@this.Url.PlainAction("Index", "LogParser", values: null)');
|
||||
});
|
||||
</script>
|
||||
}
|
||||
|
@ -48,8 +50,8 @@ else if (Model.ParseError != null)
|
|||
{
|
||||
<div class="banner error" v-pre>
|
||||
<strong>Oops, couldn't parse that log. (Make sure you upload the log file, not the console text.)</strong><br />
|
||||
Share this URL when asking for help: <code>@(new Uri(new Uri(Model.SectionUrl), Model.PasteID))</code><br />
|
||||
(Or <a href="@Model.SectionUrl">upload a new log</a>.)<br />
|
||||
Share this URL when asking for help: <code>https://@this.Context.Request.Host.ToUriComponent()@this.Url.PlainAction("Index", "LogParser", new { id = Model.PasteID }))</code><br />
|
||||
(Or <a href="@this.Url.PlainAction("Index", "LogParser", values: null)">upload a new log</a>.)<br />
|
||||
<br />
|
||||
<small v-pre>Error details: @Model.ParseError</small>
|
||||
</div>
|
||||
|
@ -57,8 +59,20 @@ else if (Model.ParseError != null)
|
|||
else if (Model.ParsedLog?.IsValid == true)
|
||||
{
|
||||
<div class="banner success" v-pre>
|
||||
<strong>Share this link to let someone else see the log:</strong> <code>@(new Uri(new Uri(Model.SectionUrl), Model.PasteID))</code><br />
|
||||
(Or <a href="@Model.SectionUrl">upload a new log</a>.)
|
||||
<strong>Share this link to let someone else see the log:</strong> <code>https://@this.Context.Request.Host.ToUriComponent()@this.Url.PlainAction("Index", "LogParser", new { id = Model.PasteID })</code><br />
|
||||
(Or <a href="@this.Url.PlainAction("Index", "LogParser", values: null)">upload a new log</a>.)
|
||||
</div>
|
||||
}
|
||||
|
||||
@* save warnings *@
|
||||
@if (Model.UploadWarning != null || Model.Expiry != null)
|
||||
{
|
||||
<div class="save-metadata" v-pre>
|
||||
@if (Model.Expiry != null)
|
||||
{
|
||||
<text>This log will expire in @((DateTime.UtcNow - Model.Expiry.Value).Humanize()). </text>
|
||||
}
|
||||
<!--@Model.UploadWarning-->
|
||||
</div>
|
||||
}
|
||||
|
||||
|
@ -71,7 +85,7 @@ else if (Model.ParsedLog?.IsValid == true)
|
|||
@foreach (Platform platform in new[] { Platform.Android, Platform.Linux, Platform.Mac, Platform.Windows })
|
||||
{
|
||||
<li>
|
||||
<input type="radio" name="os" value="@platform" id="os-@platform" checked="@(Model.DetectedPlatform == platform)"/>
|
||||
<input type="radio" name="os" value="@platform" id="os-@platform" checked="@(Model.DetectedPlatform == platform)" />
|
||||
<label for="os-@platform">@platform</label>
|
||||
</li>
|
||||
}
|
||||
|
@ -114,7 +128,7 @@ else if (Model.ParsedLog?.IsValid == true)
|
|||
</div>
|
||||
|
||||
<h2>How do I share my log?</h2>
|
||||
<form action="@Model.SectionUrl" method="post">
|
||||
<form action="@this.Url.PlainAction("PostAsync", "LogParser")" method="post">
|
||||
<ol>
|
||||
<li>
|
||||
Drag the file onto this textbox (or paste the text in):<br />
|
||||
|
@ -151,7 +165,7 @@ else if (Model.ParsedLog?.IsValid == true)
|
|||
<div class="content-packs">
|
||||
@foreach (LogModInfo contentPack in contentPackList.Where(pack => pack.HasUpdate))
|
||||
{
|
||||
<text>+ @contentPack.Name</text><br/>
|
||||
<text>+ @contentPack.Name</text><br />
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
@ -173,7 +187,7 @@ else if (Model.ParsedLog?.IsValid == true)
|
|||
<div>
|
||||
@foreach (LogModInfo contentPack in contentPackList.Where(pack => pack.HasUpdate))
|
||||
{
|
||||
<a href="@contentPack.UpdateLink" target="_blank">@contentPack.Version → @contentPack.UpdateVersion</a><br/>
|
||||
<a href="@contentPack.UpdateLink" target="_blank">@contentPack.Version → @contentPack.UpdateVersion</a><br />
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
@ -309,12 +323,12 @@ else if (Model.ParsedLog?.IsValid == true)
|
|||
}
|
||||
</table>
|
||||
|
||||
<small><a href="@(new Uri(new Uri(Model.SectionUrl), Model.PasteID))?raw=true">view raw log</a></small>
|
||||
<small><a href="@this.Url.PlainAction("Index", "LogParser", new { id = Model.PasteID, raw = true })">view raw log</a></small>
|
||||
}
|
||||
else
|
||||
{
|
||||
<pre v-pre>@Model.ParsedLog.RawText</pre>
|
||||
<small><a href="@(new Uri(new Uri(Model.SectionUrl), Model.PasteID))">view parsed log</a></small>
|
||||
<small><a href="@this.Url.PlainAction("Index", "LogParser", new { id = Model.PasteID })">view parsed log</a></small>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
@using Microsoft.Extensions.Options
|
||||
@using StardewModdingAPI.Web.Framework
|
||||
@using StardewModdingAPI.Web.Framework.ConfigModels
|
||||
@inject IOptions<SiteConfig> SiteConfig
|
||||
|
||||
|
@ -15,15 +16,15 @@
|
|||
<div id="sidebar">
|
||||
<h4>SMAPI</h4>
|
||||
<ul>
|
||||
<li><a href="@SiteConfig.Value.RootUrl">About SMAPI</a></li>
|
||||
<li><a href="@Url.PlainAction("Index", "Index")">About SMAPI</a></li>
|
||||
<li><a href="https://stardewvalleywiki.com/Modding:Index">Modding docs</a></li>
|
||||
</ul>
|
||||
|
||||
<h4>Tools</h4>
|
||||
<ul>
|
||||
<li><a href="@SiteConfig.Value.ModListUrl">Mod compatibility</a></li>
|
||||
<li><a href="@SiteConfig.Value.LogParserUrl">Log parser</a></li>
|
||||
<li><a href="@SiteConfig.Value.JsonValidatorUrl">JSON validator</a></li>
|
||||
<li><a href="@Url.PlainAction("Index", "Mods")">Mod compatibility</a></li>
|
||||
<li><a href="@Url.PlainAction("Index", "LogParser", values: null)">Log parser</a></li>
|
||||
<li><a href="@Url.PlainAction("Index", "JsonValidator", values: null)">JSON validator</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div id="content-column">
|
||||
|
|
|
@ -9,15 +9,14 @@
|
|||
*/
|
||||
{
|
||||
"Site": {
|
||||
"RootUrl": "http://localhost:59482/",
|
||||
"ModListUrl": "http://localhost:59482/mods/",
|
||||
"LogParserUrl": "http://localhost:59482/log/",
|
||||
"JsonValidatorUrl": "http://localhost:59482/json/",
|
||||
"BetaEnabled": false,
|
||||
"BetaBlurb": null
|
||||
},
|
||||
|
||||
"ApiClients": {
|
||||
"AmazonAccessKey": null,
|
||||
"AmazonSecretKey": null,
|
||||
|
||||
"GitHubUsername": null,
|
||||
"GitHubPassword": null,
|
||||
|
||||
|
|
|
@ -16,17 +16,18 @@
|
|||
},
|
||||
|
||||
"Site": {
|
||||
"RootUrl": null, // see top note
|
||||
"ModListUrl": null, // see top note
|
||||
"LogParserUrl": null, // see top note
|
||||
"JsonValidatorUrl": null, // see top note
|
||||
"BetaEnabled": null, // see top note
|
||||
"BetaBlurb": null // see top note
|
||||
"BetaEnabled": null,
|
||||
"BetaBlurb": null
|
||||
},
|
||||
|
||||
"ApiClients": {
|
||||
"UserAgent": "SMAPI/{0} (+https://smapi.io)",
|
||||
|
||||
"AmazonAccessKey": null,
|
||||
"AmazonSecretKey": null,
|
||||
"AmazonRegion": "us-east-1",
|
||||
"AmazonLogBucket": "smapi-log-parser",
|
||||
|
||||
"ChucklefishBaseUrl": "https://community.playstarbound.com",
|
||||
"ChucklefishModPageUrlFormat": "resources/{0}",
|
||||
|
||||
|
@ -34,27 +35,27 @@
|
|||
|
||||
"GitHubBaseUrl": "https://api.github.com",
|
||||
"GitHubAcceptHeader": "application/vnd.github.v3+json",
|
||||
"GitHubUsername": null, // see top note
|
||||
"GitHubPassword": null, // see top note
|
||||
"GitHubUsername": null,
|
||||
"GitHubPassword": null,
|
||||
|
||||
"ModDropApiUrl": "https://www.moddrop.com/api/mods/data",
|
||||
"ModDropModPageUrl": "https://www.moddrop.com/sdv/mod/{0}",
|
||||
|
||||
"NexusApiKey": null, // see top note
|
||||
"NexusApiKey": null,
|
||||
"NexusBaseUrl": "https://www.nexusmods.com/stardewvalley/",
|
||||
"NexusModUrlFormat": "mods/{0}",
|
||||
"NexusModScrapeUrlFormat": "mods/{0}?tab=files",
|
||||
|
||||
"PastebinBaseUrl": "https://pastebin.com/",
|
||||
"PastebinUserKey": null, // see top note
|
||||
"PastebinDevKey": null // see top note
|
||||
"PastebinUserKey": null,
|
||||
"PastebinDevKey": null
|
||||
},
|
||||
|
||||
"MongoDB": {
|
||||
"Host": null, // see top note
|
||||
"Username": null, // see top note
|
||||
"Password": null, // see top note
|
||||
"Database": null // see top note
|
||||
"Host": null,
|
||||
"Username": null,
|
||||
"Password": null,
|
||||
"Database": null
|
||||
},
|
||||
|
||||
"ModCompatibilityList": {
|
||||
|
@ -67,7 +68,6 @@
|
|||
|
||||
"ModUpdateCheck": {
|
||||
"SuccessCacheMinutes": 60,
|
||||
"ErrorCacheMinutes": 5,
|
||||
"CompatibilityPageUrl": "https://mods.smapi.io"
|
||||
"ErrorCacheMinutes": 5
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,6 +47,12 @@ table caption {
|
|||
background: #FCC;
|
||||
}
|
||||
|
||||
.save-metadata {
|
||||
margin-top: 1em;
|
||||
font-size: 0.8em;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
/*********
|
||||
** Log metadata & filters
|
||||
*********/
|
||||
|
|
|
@ -142,6 +142,7 @@ smapi.jsonValidator = function (sectionUrl, pasteID) {
|
|||
});
|
||||
|
||||
// upload form
|
||||
var submit = $("#submit");
|
||||
var input = $("#input");
|
||||
if (input.length) {
|
||||
// disable submit if it's empty
|
||||
|
|
|
@ -28,7 +28,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{09CF91E5
|
|||
ProjectSection(SolutionItems) = preProject
|
||||
..\build\common.targets = ..\build\common.targets
|
||||
..\build\find-game-folder.targets = ..\build\find-game-folder.targets
|
||||
..\build\GlobalAssemblyInfo.cs = ..\build\GlobalAssemblyInfo.cs
|
||||
..\build\prepare-install-package.targets = ..\build\prepare-install-package.targets
|
||||
..\build\prepare-nuget-package.targets = ..\build\prepare-nuget-package.targets
|
||||
EndProjectSection
|
||||
|
@ -78,6 +77,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Toolkit.CoreInterface
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Web", "SMAPI.Web\SMAPI.Web.csproj", "{80EFD92F-728F-41E0-8A5B-9F6F49A91899}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Web.LegacyRedirects", "SMAPI.Web.LegacyRedirects\SMAPI.Web.LegacyRedirects.csproj", "{159AA5A5-35C2-488C-B23F-1613C80594AE}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SharedMSBuildProjectFiles) = preSolution
|
||||
SMAPI.Internal\SMAPI.Internal.projitems*{85208f8d-6fd1-4531-be05-7142490f59fe}*SharedItemsImports = 13
|
||||
|
@ -131,6 +132,10 @@ Global
|
|||
{80EFD92F-728F-41E0-8A5B-9F6F49A91899}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{80EFD92F-728F-41E0-8A5B-9F6F49A91899}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{80EFD92F-728F-41E0-8A5B-9F6F49A91899}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{159AA5A5-35C2-488C-B23F-1613C80594AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{159AA5A5-35C2-488C-B23F-1613C80594AE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{159AA5A5-35C2-488C-B23F-1613C80594AE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{159AA5A5-35C2-488C-B23F-1613C80594AE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
|
@ -20,7 +20,7 @@ namespace StardewModdingAPI
|
|||
** Public
|
||||
****/
|
||||
/// <summary>SMAPI's current semantic version.</summary>
|
||||
public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.0.0");
|
||||
public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.0.1");
|
||||
|
||||
/// <summary>The minimum supported version of Stardew Valley.</summary>
|
||||
public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.4.0");
|
||||
|
|
|
@ -52,20 +52,34 @@ namespace StardewModdingAPI.Framework
|
|||
/// <param name="gameVersion">The game version string.</param>
|
||||
private static string GetSemanticVersionString(string gameVersion)
|
||||
{
|
||||
return GameVersion.VersionMap.TryGetValue(gameVersion, out string semanticVersion)
|
||||
? semanticVersion
|
||||
: gameVersion;
|
||||
// mapped version
|
||||
if (GameVersion.VersionMap.TryGetValue(gameVersion, out string semanticVersion))
|
||||
return semanticVersion;
|
||||
|
||||
// special case: four-part versions
|
||||
string[] parts = gameVersion.Split('.');
|
||||
if (parts.Length == 4)
|
||||
return $"{parts[0]}.{parts[1]}.{parts[2]}+{parts[3]}";
|
||||
|
||||
return gameVersion;
|
||||
}
|
||||
|
||||
/// <summary>Convert a semantic version string to the equivalent game version string.</summary>
|
||||
/// <param name="semanticVersion">The semantic version string.</param>
|
||||
private static string GetGameVersionString(string semanticVersion)
|
||||
{
|
||||
// mapped versions
|
||||
foreach (var mapping in GameVersion.VersionMap)
|
||||
{
|
||||
if (mapping.Value.Equals(semanticVersion, StringComparison.InvariantCultureIgnoreCase))
|
||||
return mapping.Key;
|
||||
}
|
||||
|
||||
// special case: four-part versions
|
||||
string[] parts = semanticVersion.Split('.', '+');
|
||||
if (parts.Length == 4)
|
||||
return $"{parts[0]}.{parts[1]}.{parts[2]}.{parts[3]}";
|
||||
|
||||
return semanticVersion;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -87,7 +87,7 @@ namespace StardewModdingAPI.Framework.ModLoading
|
|||
updateUrls.Add(mod.DataRecord.AlternativeUrl);
|
||||
|
||||
// default update URL
|
||||
updateUrls.Add("https://mods.smapi.io");
|
||||
updateUrls.Add("https://smapi.io/mods");
|
||||
|
||||
// build error
|
||||
string error = $"{reasonPhrase}. Please check for a ";
|
||||
|
|
|
@ -23,7 +23,7 @@ namespace StardewModdingAPI.Framework.Models
|
|||
#endif
|
||||
[nameof(UseBetaChannel)] = Constants.ApiVersion.IsPrerelease(),
|
||||
[nameof(GitHubProjectName)] = "Pathoschild/SMAPI",
|
||||
[nameof(WebApiBaseUrl)] = "https://api.smapi.io",
|
||||
[nameof(WebApiBaseUrl)] = "https://smapi.io/api/",
|
||||
[nameof(VerboseLogging)] = false,
|
||||
[nameof(LogNetworkTraffic)] = false,
|
||||
[nameof(DumpMetadata)] = false
|
||||
|
|
|
@ -299,7 +299,7 @@ namespace StardewModdingAPI.Framework
|
|||
if (File.Exists(Constants.FatalCrashMarker))
|
||||
{
|
||||
this.Monitor.Log("The game crashed last time you played. That can be due to bugs in the game, but if it happens repeatedly you can ask for help here: https://community.playstarbound.com/threads/108375/.", LogLevel.Error);
|
||||
this.Monitor.Log("If you ask for help, make sure to share your SMAPI log: https://log.smapi.io.", LogLevel.Error);
|
||||
this.Monitor.Log("If you ask for help, make sure to share your SMAPI log: https://smapi.io/log.", LogLevel.Error);
|
||||
this.Monitor.Log("Press any key to delete the crash data and continue playing.", LogLevel.Info);
|
||||
Console.ReadKey();
|
||||
File.Delete(Constants.FatalCrashLog);
|
||||
|
@ -962,7 +962,7 @@ namespace StardewModdingAPI.Framework
|
|||
}
|
||||
catch (IncompatibleInstructionException) // details already in trace logs
|
||||
{
|
||||
string[] updateUrls = new[] { modDatabase.GetModPageUrlFor(manifest.UniqueID), "https://mods.smapi.io" }.Where(p => p != null).ToArray();
|
||||
string[] updateUrls = new[] { modDatabase.GetModPageUrlFor(manifest.UniqueID), "https://smapi.io/mods" }.Where(p => p != null).ToArray();
|
||||
errorReasonPhrase = $"it's no longer compatible. Please check for a new version at {string.Join(" or ", updateUrls)}";
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@ The default values are mirrored in StardewModdingAPI.Framework.Models.SConfig to
|
|||
* Note: the protocol will be changed to http:// on Linux/Mac due to OpenSSL issues with the
|
||||
* game's bundled Mono.
|
||||
*/
|
||||
"WebApiBaseUrl": "https://api.smapi.io",
|
||||
"WebApiBaseUrl": "https://smapi.io/api/",
|
||||
|
||||
/**
|
||||
* Whether SMAPI should log network traffic (may be very verbose). Best combined with VerboseLogging, which includes network metadata.
|
||||
|
|
|
@ -28,6 +28,9 @@ namespace StardewModdingAPI
|
|||
/// <summary>An optional prerelease tag.</summary>
|
||||
public string PrereleaseTag => this.Version.PrereleaseTag;
|
||||
|
||||
/// <summary>Optional build metadata. This is ignored when determining version precedence.</summary>
|
||||
public string BuildMetadata => this.Version.BuildMetadata;
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
|
@ -36,10 +39,11 @@ namespace StardewModdingAPI
|
|||
/// <param name="majorVersion">The major version incremented for major API changes.</param>
|
||||
/// <param name="minorVersion">The minor version incremented for backwards-compatible changes.</param>
|
||||
/// <param name="patchVersion">The patch version for backwards-compatible bug fixes.</param>
|
||||
/// <param name="build">An optional build tag.</param>
|
||||
/// <param name="prerelease">An optional prerelease tag.</param>
|
||||
/// <param name="build">Optional build metadata. This is ignored when determining version precedence.</param>
|
||||
[JsonConstructor]
|
||||
public SemanticVersion(int majorVersion, int minorVersion, int patchVersion, string build = null)
|
||||
: this(new Toolkit.SemanticVersion(majorVersion, minorVersion, patchVersion, build)) { }
|
||||
public SemanticVersion(int majorVersion, int minorVersion, int patchVersion, string prerelease = null, string build = null)
|
||||
: this(new Toolkit.SemanticVersion(majorVersion, minorVersion, patchVersion, prerelease, build)) { }
|
||||
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="version">The semantic version string.</param>
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"warn.invalid-content-removed": "Недопустимое содержимое было удалено, чтобы предотвратить сбой (см. информацию в консоли SMAPI)"
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"warn.invalid-content-removed": "Yanlış paketlenmiş bir içerik, oyunun çökmemesi için yüklenmedi (SMAPI konsol penceresinde detaylı bilgi mevcut)."
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"warn.invalid-content-removed": "非法内容已移除以防游戏闪退(查看SMAPI控制台获得更多信息)"
|
||||
}
|
Loading…
Reference in New Issue