From 9be620e2fcdc13c6e6a918323157e21e1fed568f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 19 Oct 2016 14:33:13 -0400 Subject: [PATCH 001/186] extract build configuration from Lookup Anything mod --- stardewvalley.targets | 51 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 stardewvalley.targets diff --git a/stardewvalley.targets b/stardewvalley.targets new file mode 100644 index 00000000..1e71d06a --- /dev/null +++ b/stardewvalley.targets @@ -0,0 +1,51 @@ + + + + C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley + C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley + ~\Library\Application Support\Steam\steamapps\common\Stardew Valley\Contents\MacOS + Program + $(GamePath)\StardewModdingAPI.exe + $(GamePath) + + + + + + + + + $(GamePath)\Stardew Valley.exe + + + $(GamePath)\StardewModdingAPI.exe + + + False + $(GamePath)\xTile.dll + + + + + + + False + $(GamePath)\MonoGame.Framework.dll + + + $(GamePath)\StardewValley.exe + + + $(GamePath)\StardewModdingAPI.exe + + + $(GamePath)\xTile.dll + + + + + + + + + \ No newline at end of file From c6dbad2594620f9c25e7e548b813522c1e6c626d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 19 Oct 2016 14:46:23 -0400 Subject: [PATCH 002/186] add license + readme --- LICENSE.txt | 8 ++++++++ README.md | 31 +++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 LICENSE.txt create mode 100644 README.md diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 00000000..6463a584 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,8 @@ +The MIT License (MIT) +Copyright Pathoschild and other contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..6ea731d1 --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +**Stardew.ModBuildConfig** is an open-source NuGet package which automates the build configuration +for crossplatform [Stardew Valley](http://stardewvalley.net/) mods that use SMAPI. + +The configuration detects the operating system (Linux, Mac, or Windows) and the Stardew Valley +install path, and injects the correct references to Stardew Valley, SMAPI, and XNA/MonoGame. +It also adds a `GamePath` variable which can be used to automate mod installation during testing +if desired. + +## Contents +* [Installation](#installation) +* [Configuration](#configuration) +* [Versions](#versions) +* [See also](#see-also) + +## Installation +### New mod +_TODO_ + +### Migrating an existing mod +_TODO_ + +## Configuration +_TODO_ + +## Versions +_TODO_ + +## See also +_TODO_ +* NuGet package +* Discussion thread \ No newline at end of file From 555e0d12241f9bda0262f513fa6bc8c55cc7d5fe Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 19 Oct 2016 18:54:04 -0400 Subject: [PATCH 003/186] add NuGet package spec + icon --- assets/nuget-icon.pdn | Bin 0 -> 7401 bytes assets/nuget-icon.png | Bin 0 -> 5054 bytes stardewvalley.targets => build/smapi.targets | 0 package.nuspec | 20 +++++++++++++++++++ 4 files changed, 20 insertions(+) create mode 100644 assets/nuget-icon.pdn create mode 100644 assets/nuget-icon.png rename stardewvalley.targets => build/smapi.targets (100%) create mode 100644 package.nuspec diff --git a/assets/nuget-icon.pdn b/assets/nuget-icon.pdn new file mode 100644 index 0000000000000000000000000000000000000000..7bd5c0c543cfedf1171920a0bd4f978908d78473 GIT binary patch literal 7401 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 Uw4q36!2kdN07*qoM6N<$f`284fdBvi literal 0 HcmV?d00001 diff --git a/stardewvalley.targets b/build/smapi.targets similarity index 100% rename from stardewvalley.targets rename to build/smapi.targets diff --git a/package.nuspec b/package.nuspec new file mode 100644 index 00000000..5d8e5e52 --- /dev/null +++ b/package.nuspec @@ -0,0 +1,20 @@ + + + + Pathoschild.Stardew.ModdingBuildConfig + 1.0-alpha + MSBuild config for Stardew Valley mods + Pathoschild + Pathoschild + false + https://github.com/Pathoschild/Stardew.ModBuildConfig/blob/master/LICENSE.txt + https://github.com/Pathoschild/Stardew.ModBuildConfig + https://raw.githubusercontent.com/Pathoschild/Stardew.ModBuildConfig/1.0/assets/nuget-icon.png + Automates the build configuration for a crossplatform Stardew Valley mod that uses SMAPI. + Initial release + + + + + + \ No newline at end of file From 520c46fbe661acac7f6c7854b71d05f58a06cf64 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 19 Oct 2016 19:26:33 -0400 Subject: [PATCH 004/186] expand readme, restructure config --- README.md | 39 ++++++++++++--------- build/smapi.targets | 82 +++++++++++++++++++++++++-------------------- package.nuspec | 2 +- 3 files changed, 71 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 6ea731d1..564f508e 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,38 @@ **Stardew.ModBuildConfig** is an open-source NuGet package which automates the build configuration for crossplatform [Stardew Valley](http://stardewvalley.net/) mods that use SMAPI. -The configuration detects the operating system (Linux, Mac, or Windows) and the Stardew Valley -install path, and injects the correct references to Stardew Valley, SMAPI, and XNA/MonoGame. -It also adds a `GamePath` variable which can be used to automate mod installation during testing -if desired. +The configuration... -## Contents -* [Installation](#installation) -* [Configuration](#configuration) -* [Versions](#versions) -* [See also](#see-also) +1. detects the operating system (Linux, Mac, or Windows) and the Stardew Valley install path; +2. injects the correct references to Stardew Valley, SMAPI, and XNA/MonoGame; +3. (on Windows) configures Visual Studio so you can launch the game for debugging; +4. and adds a `GamePath` variable which can be used to automate mod installation during testing + if desired. ## Installation -### New mod -_TODO_ +### Creating a new mod +1. Create an empty library project. +2. Reference the `Pathoschild.Stardew.ModBuildConfig` NuGet package. +3. [Write your code](http://canimod.com/guides/creating-a-smapi-mod). +4. Compile on any platform. ### Migrating an existing mod -_TODO_ +1. Remove any references to `Microsoft.Xna.*`, Stardew Valley, `StardewModdingAPI`, and xTile. +2. Reference the `Pathoschild.Stardew.ModBuildConfig` NuGet package. +3. Compile on any platform. ## Configuration -_TODO_ +### Custom game path +If you customised where Stardew Valley is installed, you can add your path to the list to try. -## Versions -_TODO_ +1. Get the full path to the directory containing the Stardew Valley executable. +2. Add this section to your `.csproj` file (anywhere before the added ` + C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley + + ``` ## See also _TODO_ diff --git a/build/smapi.targets b/build/smapi.targets index 1e71d06a..6231650c 100644 --- a/build/smapi.targets +++ b/build/smapi.targets @@ -1,47 +1,57 @@ - + C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley ~\Library\Application Support\Steam\steamapps\common\Stardew Valley\Contents\MacOS - Program - $(GamePath)\StardewModdingAPI.exe - $(GamePath) - - - - - - - $(GamePath)\Stardew Valley.exe - - - $(GamePath)\StardewModdingAPI.exe - - - False - $(GamePath)\xTile.dll - - + + + + + + + + + + $(GamePath)\Stardew Valley.exe + + + $(GamePath)\StardewModdingAPI.exe + + + False + $(GamePath)\xTile.dll + + - - - - False - $(GamePath)\MonoGame.Framework.dll - - - $(GamePath)\StardewValley.exe - - - $(GamePath)\StardewModdingAPI.exe - - - $(GamePath)\xTile.dll - - + + + Program + $(GamePath)\StardewModdingAPI.exe + $(GamePath) + + + + + + + False + $(GamePath)\MonoGame.Framework.dll + + + $(GamePath)\StardewValley.exe + + + $(GamePath)\StardewModdingAPI.exe + + + $(GamePath)\xTile.dll + + + + diff --git a/package.nuspec b/package.nuspec index 5d8e5e52..a000d986 100644 --- a/package.nuspec +++ b/package.nuspec @@ -2,7 +2,7 @@ Pathoschild.Stardew.ModdingBuildConfig - 1.0-alpha + 1.0-alpha2 MSBuild config for Stardew Valley mods Pathoschild Pathoschild From e5d38b73720c58b00ea747b66be18f8b735d2e6e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 19 Oct 2016 20:16:06 -0400 Subject: [PATCH 005/186] tweak error text --- build/smapi.targets | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/build/smapi.targets b/build/smapi.targets index 6231650c..f38f1585 100644 --- a/build/smapi.targets +++ b/build/smapi.targets @@ -53,9 +53,8 @@ - + - + - \ No newline at end of file From 465bd538e476766f1114f915f955e30c9abab944 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 19 Oct 2016 20:43:35 -0400 Subject: [PATCH 006/186] add more install paths --- build/smapi.targets | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/build/smapi.targets b/build/smapi.targets index f38f1585..7d512a29 100644 --- a/build/smapi.targets +++ b/build/smapi.targets @@ -1,9 +1,18 @@ - C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley + + $(STARDEWVALLEY_DIR) + + + ~/.local/share/Steam/steamapps/common/Stardew Valley + + + ~/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS + + + C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley - ~\Library\Application Support\Steam\steamapps\common\Stardew Valley\Contents\MacOS From e3d9894bf353197169fc3e84c973e07cfb5f3aae Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 19 Oct 2016 20:56:48 -0400 Subject: [PATCH 007/186] add package links to readme --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 564f508e..c53b1ac3 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,13 @@ The configuration... ## Installation ### Creating a new mod 1. Create an empty library project. -2. Reference the `Pathoschild.Stardew.ModBuildConfig` NuGet package. +2. Reference the [`Pathoschild.Stardew.ModBuildConfig` NuGet package](https://www.nuget.org/packages/Pathoschild.Stardew.ModdingBuildConfig). 3. [Write your code](http://canimod.com/guides/creating-a-smapi-mod). 4. Compile on any platform. ### Migrating an existing mod 1. Remove any references to `Microsoft.Xna.*`, Stardew Valley, `StardewModdingAPI`, and xTile. -2. Reference the `Pathoschild.Stardew.ModBuildConfig` NuGet package. +2. Reference the [`Pathoschild.Stardew.ModBuildConfig` NuGet package](https://www.nuget.org/packages/Pathoschild.Stardew.ModdingBuildConfig). 3. Compile on any platform. ## Configuration @@ -35,6 +35,5 @@ If you customised where Stardew Valley is installed, you can add your path to th ``` ## See also -_TODO_ -* NuGet package +* [NuGet package](https://www.nuget.org/packages/Pathoschild.Stardew.ModdingBuildConfig) * Discussion thread \ No newline at end of file From 524c56ee757464b5785c4d704d4da1862b173db9 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 19 Oct 2016 22:53:49 -0400 Subject: [PATCH 008/186] correct home paths on Linux/Mac --- build/smapi.targets | 4 ++-- package.nuspec | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build/smapi.targets b/build/smapi.targets index 7d512a29..2ca7a829 100644 --- a/build/smapi.targets +++ b/build/smapi.targets @@ -5,10 +5,10 @@ $(STARDEWVALLEY_DIR) - ~/.local/share/Steam/steamapps/common/Stardew Valley + $(HOME)/.local/share/Steam/steamapps/common/Stardew Valley - ~/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS + $(HOME)/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley diff --git a/package.nuspec b/package.nuspec index a000d986..2f585ab5 100644 --- a/package.nuspec +++ b/package.nuspec @@ -2,7 +2,7 @@ Pathoschild.Stardew.ModdingBuildConfig - 1.0-alpha2 + 1.0-alpha4 MSBuild config for Stardew Valley mods Pathoschild Pathoschild From 502559146bfb3cd39ee941245c3f71a7798a0760 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 21 Oct 2016 01:25:21 -0400 Subject: [PATCH 009/186] add GOG path on Linux --- build/smapi.targets | 1 + 1 file changed, 1 insertion(+) diff --git a/build/smapi.targets b/build/smapi.targets index 2ca7a829..2c0ca5b7 100644 --- a/build/smapi.targets +++ b/build/smapi.targets @@ -5,6 +5,7 @@ $(STARDEWVALLEY_DIR) + $(HOME)/GOG Games/Stardew Valley/game $(HOME)/.local/share/Steam/steamapps/common/Stardew Valley From 8d532e1cabe374a5b8673f07249e2d3848bc1a78 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 21 Oct 2016 01:25:41 -0400 Subject: [PATCH 010/186] mention mod builder compatibility in readme --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index c53b1ac3..73aa082a 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,11 @@ If you customised where Stardew Valley is installed, you can add your path to th ``` +### Compatibility with mod builders +The configuration is designed for compatibility with third-party mod compilers. [Silverplum](https://github.com/rumangerst/SilVerPLuM) +is officially supported, and you can inject the `GAMEPATH` environment variable to override the +detected game path. + ## See also * [NuGet package](https://www.nuget.org/packages/Pathoschild.Stardew.ModdingBuildConfig) * Discussion thread \ No newline at end of file From f9c107b751476b799a87cf25f9ed2424ab1a4775 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 21 Oct 2016 01:41:26 -0400 Subject: [PATCH 011/186] bump to 1.0 --- README.md | 6 +++--- package.nuspec | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 73aa082a..e1b065ec 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,13 @@ The configuration... ## Installation ### Creating a new mod 1. Create an empty library project. -2. Reference the [`Pathoschild.Stardew.ModBuildConfig` NuGet package](https://www.nuget.org/packages/Pathoschild.Stardew.ModdingBuildConfig). +2. Reference the [`Pathoschild.Stardew.ModBuildConfig` NuGet package](https://www.nuget.org/packages/Pathoschild.Stardew.ModBuildConfig). 3. [Write your code](http://canimod.com/guides/creating-a-smapi-mod). 4. Compile on any platform. ### Migrating an existing mod 1. Remove any references to `Microsoft.Xna.*`, Stardew Valley, `StardewModdingAPI`, and xTile. -2. Reference the [`Pathoschild.Stardew.ModBuildConfig` NuGet package](https://www.nuget.org/packages/Pathoschild.Stardew.ModdingBuildConfig). +2. Reference the [`Pathoschild.Stardew.ModBuildConfig` NuGet package](https://www.nuget.org/packages/Pathoschild.Stardew.ModBuildConfig). 3. Compile on any platform. ## Configuration @@ -40,5 +40,5 @@ is officially supported, and you can inject the `GAMEPATH` environment variable detected game path. ## See also -* [NuGet package](https://www.nuget.org/packages/Pathoschild.Stardew.ModdingBuildConfig) +* [NuGet package](https://www.nuget.org/packages/Pathoschild.Stardew.ModBuildConfig) * Discussion thread \ No newline at end of file diff --git a/package.nuspec b/package.nuspec index 2f585ab5..05c478de 100644 --- a/package.nuspec +++ b/package.nuspec @@ -1,13 +1,13 @@ - Pathoschild.Stardew.ModdingBuildConfig - 1.0-alpha4 + Pathoschild.Stardew.ModBuildConfig + 1.0 MSBuild config for Stardew Valley mods Pathoschild Pathoschild false - https://github.com/Pathoschild/Stardew.ModBuildConfig/blob/master/LICENSE.txt + https://github.com/Pathoschild/Stardew.ModBuildConfig/blob/1.0/LICENSE.txt https://github.com/Pathoschild/Stardew.ModBuildConfig https://raw.githubusercontent.com/Pathoschild/Stardew.ModBuildConfig/1.0/assets/nuget-icon.png Automates the build configuration for a crossplatform Stardew Valley mod that uses SMAPI. From 137d795ab3d01bc132e99e19ba9567fd70f3cec3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 21 Oct 2016 01:48:57 -0400 Subject: [PATCH 012/186] fix package --- package.nuspec | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.nuspec b/package.nuspec index 05c478de..58701695 100644 --- a/package.nuspec +++ b/package.nuspec @@ -2,7 +2,7 @@ Pathoschild.Stardew.ModBuildConfig - 1.0 + 1.0.1 MSBuild config for Stardew Valley mods Pathoschild Pathoschild @@ -11,10 +11,10 @@ https://github.com/Pathoschild/Stardew.ModBuildConfig https://raw.githubusercontent.com/Pathoschild/Stardew.ModBuildConfig/1.0/assets/nuget-icon.png Automates the build configuration for a crossplatform Stardew Valley mod that uses SMAPI. - Initial release + Corrected package structure - + \ No newline at end of file From cb9efa4e8266b67ddb5c19a107af389cdedce49c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 21 Oct 2016 19:42:09 -0400 Subject: [PATCH 013/186] add support for platform targeting --- README.md | 55 +++++++++++++++++++++++++++++++++++++-------- build/smapi.targets | 31 +++++++++++++++++++++---- package.nuspec | 8 +++---- 3 files changed, 77 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index e1b065ec..b94c118b 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,18 @@ **Stardew.ModBuildConfig** is an open-source NuGet package which automates the build configuration for crossplatform [Stardew Valley](http://stardewvalley.net/) mods that use SMAPI. -The configuration... +## Usage +Basically this package lets you write your mod once, and compile it on any computer. It detects +your current platform (Linux, Mac, or Windows) and game path, and injects the right references +automatically. You can also target a specific platform to create a mod package compatible with that +platform. -1. detects the operating system (Linux, Mac, or Windows) and the Stardew Valley install path; -2. injects the correct references to Stardew Valley, SMAPI, and XNA/MonoGame; -3. (on Windows) configures Visual Studio so you can launch the game for debugging; -4. and adds a `GamePath` variable which can be used to automate mod installation during testing - if desired. +More specifically, the configuration... + +1. detects the operating system and Stardew Valley path; +2. injects the right references to Stardew Valley, SMAPI, and XNA/MonoGame for your platform; +3. configures Visual Studio so you can launch the game for debugging (_Windows only_); +4. and adds a `GamePath` variable which can be used to script mod packaging if desired. ## Installation ### Creating a new mod @@ -23,7 +28,7 @@ The configuration... ## Configuration ### Custom game path -If you customised where Stardew Valley is installed, you can add your path to the list to try. +If you customised where Stardew Valley is installed, you can specify where it is. 1. Get the full path to the directory containing the Stardew Valley executable. 2. Add this section to your `.csproj` file (anywhere before the added ` ``` +The configuration will check your custom path first, then fall back to the default paths. (That way +you can still compile it normally on a different computer.) + +### Target platform +By default the build configuration will target your current platform (e.g. Linux, Mac, or Windows). +If you're compiling it for a different platform (and have the required dependencies installed), you +can manually override the platform detection. + +You can define it... + +* in your `.csproj` (anywhere before the added ` + Windows + + ``` + +* _or_ by setting one of these compile constant: `GAME_PLATFORM_LINUX`, `GAME_PLATFORM_MAC`, or + `GAME_PLATFORM_WINDOWS`. + * In Visual Studio: right-click on the project and choose _Properties_. Click the _Build_ + tab, and enter the constants into the _Conditional compilation symbols_ field. + * In MonoDevelop: right-click on the project and choose _Options. Click the + _Build » Compiler_ tab, and enter the constants into the _Define Symbols_ field. + ### Compatibility with mod builders The configuration is designed for compatibility with third-party mod compilers. [Silverplum](https://github.com/rumangerst/SilVerPLuM) -is officially supported, and you can inject the `GAMEPATH` environment variable to override the -detected game path. +is officially supported, and mod builds can set the following environment variables: + +* `GAMEPATH`: overrides the Stardew Valley install path. +* `GAMEPLATFORM`: overrides the detected platform. Should be only of `Linux`, `Mac`, or `Windows`. + +## Versions +* 1.0: initial release. +* 1.1: added support for targeting platforms. ## See also * [NuGet package](https://www.nuget.org/packages/Pathoschild.Stardew.ModBuildConfig) diff --git a/build/smapi.targets b/build/smapi.targets index 2c0ca5b7..074581b4 100644 --- a/build/smapi.targets +++ b/build/smapi.targets @@ -1,5 +1,23 @@ - + + + + + Linux + Mac + Windows + + + Linux + Mac + Windows + + + $(STARDEWVALLEY_DIR) @@ -16,9 +34,11 @@ C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley - + - + @@ -63,8 +83,11 @@ - + + \ No newline at end of file diff --git a/package.nuspec b/package.nuspec index 58701695..3236e636 100644 --- a/package.nuspec +++ b/package.nuspec @@ -2,16 +2,16 @@ Pathoschild.Stardew.ModBuildConfig - 1.0.1 + 1.1 MSBuild config for Stardew Valley mods Pathoschild Pathoschild false - https://github.com/Pathoschild/Stardew.ModBuildConfig/blob/1.0/LICENSE.txt + https://github.com/Pathoschild/Stardew.ModBuildConfig/blob/1.1/LICENSE.txt https://github.com/Pathoschild/Stardew.ModBuildConfig - https://raw.githubusercontent.com/Pathoschild/Stardew.ModBuildConfig/1.0/assets/nuget-icon.png + https://raw.githubusercontent.com/Pathoschild/Stardew.ModBuildConfig/1.1/assets/nuget-icon.png Automates the build configuration for a crossplatform Stardew Valley mod that uses SMAPI. - Corrected package structure + Added support for targeting platforms. From 9db2bbc94168ea4770fb0714f51a6fa813bb9e04 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 24 Oct 2016 10:51:54 -0400 Subject: [PATCH 014/186] no longer copy game binaries to build output --- README.md | 2 +- build/smapi.targets | 23 ++++++++++++++++++----- package.nuspec | 8 ++++---- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index b94c118b..0894c49d 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ You can define it... `GAME_PLATFORM_WINDOWS`. * In Visual Studio: right-click on the project and choose _Properties_. Click the _Build_ tab, and enter the constants into the _Conditional compilation symbols_ field. - * In MonoDevelop: right-click on the project and choose _Options. Click the + * In MonoDevelop: right-click on the project and choose _Options_. Click the _Build » Compiler_ tab, and enter the constants into the _Define Symbols_ field. ### Compatibility with mod builders diff --git a/build/smapi.targets b/build/smapi.targets index 074581b4..0bd8e0ae 100644 --- a/build/smapi.targets +++ b/build/smapi.targets @@ -41,18 +41,27 @@ - - - + + false + + + false + + + false + $(GamePath)\Stardew Valley.exe + false $(GamePath)\StardewModdingAPI.exe + false - False $(GamePath)\xTile.dll + false + False @@ -67,17 +76,21 @@ - False $(GamePath)\MonoGame.Framework.dll + false + False $(GamePath)\StardewValley.exe + false $(GamePath)\StardewModdingAPI.exe + false $(GamePath)\xTile.dll + false diff --git a/package.nuspec b/package.nuspec index 3236e636..8072ad38 100644 --- a/package.nuspec +++ b/package.nuspec @@ -2,16 +2,16 @@ Pathoschild.Stardew.ModBuildConfig - 1.1 + 1.2 MSBuild config for Stardew Valley mods Pathoschild Pathoschild false - https://github.com/Pathoschild/Stardew.ModBuildConfig/blob/1.1/LICENSE.txt + https://github.com/Pathoschild/Stardew.ModBuildConfig/blob/1.2/LICENSE.txt https://github.com/Pathoschild/Stardew.ModBuildConfig - https://raw.githubusercontent.com/Pathoschild/Stardew.ModBuildConfig/1.1/assets/nuget-icon.png + https://raw.githubusercontent.com/Pathoschild/Stardew.ModBuildConfig/1.2/assets/nuget-icon.png Automates the build configuration for a crossplatform Stardew Valley mod that uses SMAPI. - Added support for targeting platforms. + No longer copies game binaries to build output. From d3946c3324cf6fc0c73590b22770da061b025331 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 5 Dec 2016 11:57:12 -0500 Subject: [PATCH 015/186] add 'simplify mod development' section, add TOC --- README.md | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0894c49d..1b106f1c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,12 @@ **Stardew.ModBuildConfig** is an open-source NuGet package which automates the build configuration for crossplatform [Stardew Valley](http://stardewvalley.net/) mods that use SMAPI. +## Contents +* [Usage](#usage) +* [Installation](#installation) +* [Configuration](#configuration) +* [Simplify mod development](#simplify-mod-development) + ## Usage Basically this package lets you write your mod once, and compile it on any computer. It detects your current platform (Linux, Mac, or Windows) and game path, and injects the right references @@ -72,10 +78,40 @@ is officially supported, and mod builds can set the following environment variab * `GAMEPATH`: overrides the Stardew Valley install path. * `GAMEPLATFORM`: overrides the detected platform. Should be only of `Linux`, `Mac`, or `Windows`. +## Simplify mod development +### Package your mod into the game directory automatically +During development, it's helpful to have the mod files packaged into your `Mods` directory automatically each time you build. To do that: + +1. Edit your mod's `.csproj` file. +2. Add this block of code at the bottom, right above the closing `` tag: + + ```cs + + + $(GamePath)\Mods\$(TargetName) + + + + + + + ``` +3. Optionally, edit the `` value to change the name, or add any additional files your mod needs. + +That's it! Each time you build, the files in `\Mods\` will be updated. + +### Debugging +Debugging into your mod code when the game is running is pretty straightforward, since this package injects some of the configuration automatically. To do that: + +1. [Package your mod into the game directory automatically](#package-your-mod-into-the-game-directory-automatically). +2. Launch the project with debugging in Visual Studio or MonoDevelop. + +This will deploy your mod files into the game directory, launch SMAPI, and attach a debugger automatically. Now you can step through your code, set breakpoints, etc. + ## Versions * 1.0: initial release. * 1.1: added support for targeting platforms. ## See also * [NuGet package](https://www.nuget.org/packages/Pathoschild.Stardew.ModBuildConfig) -* Discussion thread \ No newline at end of file +* Discussion thread From 0fe3275037b03fefb4e4c09a30097a85a12a3c03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20M=C3=BCssig?= Date: Tue, 6 Dec 2016 21:04:50 +0100 Subject: [PATCH 016/186] add support for non default Stardew Valley pathes (Steam and gog) --- build/smapi.targets | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/build/smapi.targets b/build/smapi.targets index 0bd8e0ae..759bd104 100644 --- a/build/smapi.targets +++ b/build/smapi.targets @@ -30,9 +30,11 @@ $(HOME)/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS - C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley - C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley - + C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley + $([" >$([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\GOG.com\Games\1453375253}', 'PATH', null,RegistryView.Registry64, RegistryView.Registry32)) + C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley + $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150', 'InstallLocation', null,RegistryView.Registry64, RegistryView.Registry32)) + C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley - $([" >$([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\GOG.com\Games\1453375253}', 'PATH', null,RegistryView.Registry64, RegistryView.Registry32)) C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley - $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150', 'InstallLocation', null,RegistryView.Registry64, RegistryView.Registry32)) - + $(Registry:HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\GOG.com\Games\1453375253@PATH) + $(Registry:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150@InstallLocation) + C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley - $(Registry:HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\GOG.com\Games\1453375253@PATH) - $(Registry:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150@InstallLocation) + $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\GOG.com\Games\1453375253}', 'PATH', null, RegistryView.Registry32)) + $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150', 'InstallLocation', null,RegistryView.Registry64, RegistryView.Registry32)) C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley - $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\GOG.com\Games\1453375253}', 'PATH', null, RegistryView.Registry32)) - $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150', 'InstallLocation', null,RegistryView.Registry64, RegistryView.Registry32)) + $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\GOG.com\Games\1453375253', 'PATH', null, RegistryView.Registry32)) + $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\GOG.com\Games\1453375253', 'PATH', null, RegistryView.Registry64) + $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150', 'InstallLocation', null, RegistryView.Registry64, RegistryView.Registry32)) - - $(STARDEWVALLEY_DIR) - $(HOME)/GOG Games/Stardew Valley/game $(HOME)/.local/share/Steam/steamapps/common/Stardew Valley diff --git a/package.nuspec b/package.nuspec index fb009841..54894d90 100644 --- a/package.nuspec +++ b/package.nuspec @@ -11,7 +11,7 @@ https://github.com/Pathoschild/Stardew.ModBuildConfig https://raw.githubusercontent.com/Pathoschild/Stardew.ModBuildConfig/1.3/assets/nuget-icon.png Automates the build configuration for a crossplatform Stardew Valley mod that uses SMAPI. - Fix game path detection on 32-bit Windows. + Fix game path detection on 32-bit Windows; remove support for SilVerPLuM (discontinued). From 93dcf1f1e18441a6eeb585d0d77888248370f429 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 10 Jan 2017 17:13:58 -0500 Subject: [PATCH 022/186] remove support custom target platform (never used) --- README.md | 24 +----------------------- build/smapi.targets | 17 +++++------------ package.nuspec | 2 +- 3 files changed, 7 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index a02f6f97..bd6feaa2 100644 --- a/README.md +++ b/README.md @@ -48,29 +48,6 @@ If you customised where Stardew Valley is installed, you can specify where it is The configuration will check your custom path first, then fall back to the default paths. (That way you can still compile it normally on a different computer.) -### Target platform -By default the build configuration will target your current platform (e.g. Linux, Mac, or Windows). -If you're compiling it for a different platform (and have the required dependencies installed), you -can manually override the platform detection. - -You can define it... - -* in your `.csproj` (anywhere before the added ` - Windows - - ``` - -* _or_ by setting one of these compile constant: `GAME_PLATFORM_LINUX`, `GAME_PLATFORM_MAC`, or - `GAME_PLATFORM_WINDOWS`. - * In Visual Studio: right-click on the project and choose _Properties_. Click the _Build_ - tab, and enter the constants into the _Conditional compilation symbols_ field. - * In MonoDevelop: right-click on the project and choose _Options_. Click the - _Build » Compiler_ tab, and enter the constants into the _Define Symbols_ field. - ## Simplify mod development ### Package your mod into the game directory automatically During development, it's helpful to have the mod files packaged into your `Mods` directory automatically each time you build. To do that: @@ -105,6 +82,7 @@ This will deploy your mod files into the game directory, launch SMAPI, and attac 1.3: * Fixed non-default game paths on 32-bit Windows. * Removed support for SilVerPLuM (discontinued). +* Removed support for overriding the target platform (never used). 1.2: * Added support for non-default game paths on Windows by reading the registry. diff --git a/build/smapi.targets b/build/smapi.targets index 93588445..3c6717da 100644 --- a/build/smapi.targets +++ b/build/smapi.targets @@ -3,16 +3,9 @@ ## select target OS #######--> - - - Linux - Mac - Windows - - - Linux - Mac - Windows + Linux + Mac + Windows - C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley - C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley + C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley + C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\GOG.com\Games\1453375253', 'PATH', null, RegistryView.Registry32)) $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150', 'InstallLocation', null, RegistryView.Registry64, RegistryView.Registry32)) diff --git a/package.nuspec b/package.nuspec index 54894d90..5c1f5d1b 100644 --- a/package.nuspec +++ b/package.nuspec @@ -11,7 +11,7 @@ https://github.com/Pathoschild/Stardew.ModBuildConfig https://raw.githubusercontent.com/Pathoschild/Stardew.ModBuildConfig/1.3/assets/nuget-icon.png Automates the build configuration for a crossplatform Stardew Valley mod that uses SMAPI. - Fix game path detection on 32-bit Windows; remove support for SilVerPLuM (discontinued). + Fix game path detection on 32-bit Windows; remove support for SilVerPLuM (discontinued); removed support for overriding target platform (never used). From 34d2881aeed5afcb8f93853726f8a1f725a592a9 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 10 Jan 2017 17:59:24 -0500 Subject: [PATCH 023/186] tweak custom game path section --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index bd6feaa2..83b79ea9 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ for crossplatform [Stardew Valley](http://stardewvalley.net/) mods that use SMAP ## Contents * [Usage](#usage) * [Installation](#installation) -* [Configuration](#configuration) +* [Custom game path](#custom-game-path) * [Simplify mod development](#simplify-mod-development) ## Usage @@ -32,21 +32,21 @@ More specifically, the configuration... 2. Reference the [`Pathoschild.Stardew.ModBuildConfig` NuGet package](https://www.nuget.org/packages/Pathoschild.Stardew.ModBuildConfig). 3. Compile on any platform. -## Configuration -### Custom game path -If you customised where Stardew Valley is installed, you can specify where it is. +## Custom game path +The package should automatically detect your game path. If it can't for some reason, you +can specify it manually. -1. Get the full path to the directory containing the Stardew Valley executable. -2. Add this section to your `.csproj` file (anywhere before the added ` C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley ``` -The configuration will check your custom path first, then fall back to the default paths. (That way -you can still compile it normally on a different computer.) +The configuration will check your custom path first, then fall back to the default paths (so it'll +still compile on a different computer). ## Simplify mod development ### Package your mod into the game directory automatically @@ -55,7 +55,7 @@ During development, it's helpful to have the mod files packaged into your `Mods` 1. Edit your mod's `.csproj` file. 2. Add this block of code at the bottom, right above the closing `` tag: - ```cs + ```xml $(GamePath)\Mods\$(TargetName) From db2cc7ef41aa2ce84e1c610db8d5f6cba3092a45 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 11 Jan 2017 13:46:21 -0500 Subject: [PATCH 024/186] rewrite readme for simplicity --- README.md | 91 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 46 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 83b79ea9..7829cd93 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,36 @@ **Stardew.ModBuildConfig** is an open-source NuGet package which automates the build configuration -for crossplatform [Stardew Valley](http://stardewvalley.net/) mods that use SMAPI. +for [Stardew Valley](http://stardewvalley.net/) [SMAPI](https://github.com/Pathoschild/SMAPI) mods. + +The package... + +* lets you write your mod once, and compile it on any computer. It detects the current platform + (Linux, Mac, or Windows) and game install path, and injects the right references automatically. +* configures Visual Studio so you can debug into the mod code when the game is running (_Windows + only_). +* packages the mod automatically into the game's mod folder when you build the code (_optional_). + ## Contents -* [Usage](#usage) -* [Installation](#installation) -* [Custom game path](#custom-game-path) +* [Install](#install) * [Simplify mod development](#simplify-mod-development) +* [Troubleshoot](#troubleshoot) +* [Versions](#versions) -## Usage -Basically this package lets you write your mod once, and compile it on any computer. It detects -your current platform (Linux, Mac, or Windows) and game path, and injects the right references -automatically. You can also target a specific platform to create a mod package compatible with that -platform. +## Install +**When creating a new mod:** -More specifically, the configuration... - -1. detects the operating system and Stardew Valley path; -2. injects the right references to Stardew Valley, SMAPI, and XNA/MonoGame for your platform; -3. configures Visual Studio so you can launch the game for debugging (_Windows only_); -4. and adds a `GamePath` variable which can be used to script mod packaging if desired. - -## Installation -### Creating a new mod 1. Create an empty library project. 2. Reference the [`Pathoschild.Stardew.ModBuildConfig` NuGet package](https://www.nuget.org/packages/Pathoschild.Stardew.ModBuildConfig). 3. [Write your code](http://canimod.com/guides/creating-a-smapi-mod). 4. Compile on any platform. -### Migrating an existing mod -1. Remove any references to `Microsoft.Xna.*`, Stardew Valley, `StardewModdingAPI`, and xTile. +**When migrating an existing mod:** + +1. Remove any project references to `Microsoft.Xna.*`, `MonoGame`, Stardew Valley, + `StardewModdingAPI`, and `xTile`. 2. Reference the [`Pathoschild.Stardew.ModBuildConfig` NuGet package](https://www.nuget.org/packages/Pathoschild.Stardew.ModBuildConfig). 3. Compile on any platform. -## Custom game path -The package should automatically detect your game path. If it can't for some reason, you -can specify it manually. - -1. Get the full folder path containing the Stardew Valley executable. -2. Add this to your `.csproj` file under the ` - C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley - - ``` - -The configuration will check your custom path first, then fall back to the default paths (so it'll -still compile on a different computer). - ## Simplify mod development ### Package your mod into the game directory automatically During development, it's helpful to have the mod files packaged into your `Mods` directory automatically each time you build. To do that: @@ -70,22 +53,44 @@ During development, it's helpful to have the mod files packaged into your `Mods` That's it! Each time you build, the files in `\Mods\` will be updated. -### Debugging -Debugging into your mod code when the game is running is pretty straightforward, since this package injects some of the configuration automatically. To do that: +### Debug into the mod code +Stepping into your mod code when the game is running is straightforward, since this package injects the configuration automatically. To do it: 1. [Package your mod into the game directory automatically](#package-your-mod-into-the-game-directory-automatically). 2. Launch the project with debugging in Visual Studio or MonoDevelop. This will deploy your mod files into the game directory, launch SMAPI, and attach a debugger automatically. Now you can step through your code, set breakpoints, etc. +## Troubleshoot +### "Failed to find the game install path" +If you see this error: + +> Failed to find the game install path automatically; edit the *.csproj file and manually add a +> <GamePath> setting with the full directory path containing the Stardew Valley executable. + +...the package couldn't find your game install path automatically. You'll need to specify the +game location: + +1. Get the full folder path containing the Stardew Valley executable. +2. Add this to your `.csproj` file under the ` + C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley + + ``` + +The configuration will check your custom path first, then fall back to the default paths (so it'll +still compile on a different computer). + ## Versions 1.3: -* Fixed non-default game paths on 32-bit Windows. +* Fixed detection of non-default game paths on 32-bit Windows. * Removed support for SilVerPLuM (discontinued). -* Removed support for overriding the target platform (never used). +* Removed support for overriding the target platform (no longer needed since SMAPI crossplatforms mods automatically). 1.2: -* Added support for non-default game paths on Windows by reading the registry. +* Added support for non-default game paths on Windows. 1.1: * Added support for overriding the target platform. @@ -95,7 +100,3 @@ This will deploy your mod files into the game directory, launch SMAPI, and attac * Added support for detecting the game path automatically. * Added support for injecting XNA/MonoGame references automatically based on the OS. * Added support for mod builders like SilVerPLuM. - -## See also -* [NuGet package](https://www.nuget.org/packages/Pathoschild.Stardew.ModBuildConfig) -* Discussion thread From 6f0900b34c1964a9bd047cef0555d44c63f72343 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 11 Jan 2017 14:06:46 -0500 Subject: [PATCH 025/186] update nuspec --- package.nuspec | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/package.nuspec b/package.nuspec index 5c1f5d1b..4f5b47fd 100644 --- a/package.nuspec +++ b/package.nuspec @@ -7,14 +7,14 @@ Pathoschild Pathoschild false - https://github.com/Pathoschild/Stardew.ModBuildConfig/blob/1.3/LICENSE.txt + https://github.com/Pathoschild/Stardew.ModBuildConfig/blob/1.4/LICENSE.txt https://github.com/Pathoschild/Stardew.ModBuildConfig - https://raw.githubusercontent.com/Pathoschild/Stardew.ModBuildConfig/1.3/assets/nuget-icon.png - Automates the build configuration for a crossplatform Stardew Valley mod that uses SMAPI. - Fix game path detection on 32-bit Windows; remove support for SilVerPLuM (discontinued); removed support for overriding target platform (never used). + https://raw.githubusercontent.com/Pathoschild/Stardew.ModBuildConfig/1.4/assets/nuget-icon.png + Automates the build configuration for crossplatform Stardew Valley SMAPI mods. + Fixed detection of non-default game paths on 32-bit Windows; removed support for SilVerPLuM (discontinued); removed support for overriding the target platform (no longer needed). - \ No newline at end of file + From 374f89eb688c03960ac81854d01529f111dcb9e4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 11 Jan 2017 14:43:11 -0500 Subject: [PATCH 026/186] fix release notes --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7829cd93..59b1eba7 100644 --- a/README.md +++ b/README.md @@ -84,14 +84,17 @@ The configuration will check your custom path first, then fall back to the defau still compile on a different computer). ## Versions -1.3: +1.4: * Fixed detection of non-default game paths on 32-bit Windows. * Removed support for SilVerPLuM (discontinued). * Removed support for overriding the target platform (no longer needed since SMAPI crossplatforms mods automatically). -1.2: +1.3: * Added support for non-default game paths on Windows. +1.2: +* Exclude game binaries from mod build output. + 1.1: * Added support for overriding the target platform. From f37733df11873250233a2cf84a118cfcd37dd643 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 22 Jan 2017 22:18:47 -0500 Subject: [PATCH 027/186] Add GOG Mac path Thanks to LeonBlade for the path. --- README.md | 3 +++ build/smapi.targets | 1 + 2 files changed, 4 insertions(+) diff --git a/README.md b/README.md index 59b1eba7..f3be04b3 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,9 @@ The configuration will check your custom path first, then fall back to the defau still compile on a different computer). ## Versions +1.5 (upcoming): +* Added default GOG path on Mac. + 1.4: * Fixed detection of non-default game paths on 32-bit Windows. * Removed support for SilVerPLuM (discontinued). diff --git a/build/smapi.targets b/build/smapi.targets index 3c6717da..ed513846 100644 --- a/build/smapi.targets +++ b/build/smapi.targets @@ -17,6 +17,7 @@ $(HOME)/.local/share/Steam/steamapps/common/Stardew Valley + /Applications/Stardew Valley.app/Contents/MacOS $(HOME)/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS From 2c278b970385486232b2c2727ab74c20e7730921 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 22 Jan 2017 22:52:41 -0500 Subject: [PATCH 028/186] add support for setting a custom game path globally --- README.md | 49 +++++++++++++++++++++++++++++++++------------ build/smapi.targets | 8 +++++++- 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index f3be04b3..f472049c 100644 --- a/README.md +++ b/README.md @@ -63,28 +63,51 @@ This will deploy your mod files into the game directory, launch SMAPI, and attac ## Troubleshoot ### "Failed to find the game install path" -If you see this error: +That error means the package couldn't figure out where the game is installed. You need to specify +the game location yourself. There's two ways to do that: -> Failed to find the game install path automatically; edit the *.csproj file and manually add a -> <GamePath> setting with the full directory path containing the Stardew Valley executable. +* **Option 1: set the path in the project file.** + _(You'll need to do it for every project that uses the package.)_ + 1. Get the folder path containing the Stardew Valley `.exe` file. + 2. Add this to your `.csproj` file under the ` + PATH_HERE + + ``` -1. Get the full folder path containing the Stardew Valley executable. -2. Add this to your `.csproj` file under the ` - C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley - - ``` + 3. Replace `PATH_HERE` with your custom game install path. + +* **Option 2: set the path globally.** + _This will apply to every project that uses version 1.5+ of package._ + + 1. Get the full folder path containing the Stardew Valley executable. + 2. Create this file path: + + platform | path + --------- | ---- + Linux/Mac | `~/stardewvalley.targets` + Windows | `%USERPROFILE%\stardewvalley.targets` + + 3. Save the file with this content: + + ```xml + + + PATH_HERE + + + ``` + + 4. Replace `PATH_HERE` with your custom game install path. The configuration will check your custom path first, then fall back to the default paths (so it'll still compile on a different computer). ## Versions 1.5 (upcoming): +* Added support for setting a custom game path globally. * Added default GOG path on Mac. 1.4: diff --git a/build/smapi.targets b/build/smapi.targets index ed513846..de3420d1 100644 --- a/build/smapi.targets +++ b/build/smapi.targets @@ -1,4 +1,10 @@ + + + + @@ -94,6 +100,6 @@ #######--> - + \ No newline at end of file From f681efd6e7950c5a1b40990904b6bcdc3c04818f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 22 Jan 2017 22:54:07 -0500 Subject: [PATCH 029/186] update nuspec --- package.nuspec | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.nuspec b/package.nuspec index 4f5b47fd..5967c2e3 100644 --- a/package.nuspec +++ b/package.nuspec @@ -2,16 +2,16 @@ Pathoschild.Stardew.ModBuildConfig - 1.4 + 1.5 MSBuild config for Stardew Valley mods Pathoschild Pathoschild false - https://github.com/Pathoschild/Stardew.ModBuildConfig/blob/1.4/LICENSE.txt + https://github.com/Pathoschild/Stardew.ModBuildConfig/blob/1.5/LICENSE.txt https://github.com/Pathoschild/Stardew.ModBuildConfig - https://raw.githubusercontent.com/Pathoschild/Stardew.ModBuildConfig/1.4/assets/nuget-icon.png + https://raw.githubusercontent.com/Pathoschild/Stardew.ModBuildConfig/1.5/assets/nuget-icon.png Automates the build configuration for crossplatform Stardew Valley SMAPI mods. - Fixed detection of non-default game paths on 32-bit Windows; removed support for SilVerPLuM (discontinued); removed support for overriding the target platform (no longer needed). + Added support for setting a custom game path globally; added default GOG path on Mac. From 8785ebd11749ddcfb2786085535617dcde35eb15 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 5 Jun 2017 14:59:19 -0400 Subject: [PATCH 030/186] recommended path override with config instead of per-project setting --- README.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index f472049c..2c392150 100644 --- a/README.md +++ b/README.md @@ -66,20 +66,7 @@ This will deploy your mod files into the game directory, launch SMAPI, and attac That error means the package couldn't figure out where the game is installed. You need to specify the game location yourself. There's two ways to do that: -* **Option 1: set the path in the project file.** - _(You'll need to do it for every project that uses the package.)_ - 1. Get the folder path containing the Stardew Valley `.exe` file. - 2. Add this to your `.csproj` file under the ` - PATH_HERE - - ``` - - 3. Replace `PATH_HERE` with your custom game install path. - -* **Option 2: set the path globally.** +* **Option 1: set the path globally.** _This will apply to every project that uses version 1.5+ of package._ 1. Get the full folder path containing the Stardew Valley executable. @@ -105,6 +92,19 @@ the game location yourself. There's two ways to do that: The configuration will check your custom path first, then fall back to the default paths (so it'll still compile on a different computer). +* **Option 2: set the path in the project file.** + _(You'll need to do it for every project that uses the package.)_ + 1. Get the folder path containing the Stardew Valley `.exe` file. + 2. Add this to your `.csproj` file under the ` + PATH_HERE + + ``` + + 3. Replace `PATH_HERE` with your custom game install path. + ## Versions 1.5 (upcoming): * Added support for setting a custom game path globally. From 85a51f23c4eabe816345f504133e0d5cdba0633f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 5 Jun 2017 15:02:40 -0400 Subject: [PATCH 031/186] move release notes into separate file --- README.md | 24 +----------------------- release-notes.md | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 23 deletions(-) create mode 100644 release-notes.md diff --git a/README.md b/README.md index 2c392150..351ebd91 100644 --- a/README.md +++ b/README.md @@ -106,26 +106,4 @@ still compile on a different computer). 3. Replace `PATH_HERE` with your custom game install path. ## Versions -1.5 (upcoming): -* Added support for setting a custom game path globally. -* Added default GOG path on Mac. - -1.4: -* Fixed detection of non-default game paths on 32-bit Windows. -* Removed support for SilVerPLuM (discontinued). -* Removed support for overriding the target platform (no longer needed since SMAPI crossplatforms mods automatically). - -1.3: -* Added support for non-default game paths on Windows. - -1.2: -* Exclude game binaries from mod build output. - -1.1: -* Added support for overriding the target platform. - -1.0: -* Initial release. -* Added support for detecting the game path automatically. -* Added support for injecting XNA/MonoGame references automatically based on the OS. -* Added support for mod builders like SilVerPLuM. +See [release notes](release-notes.md). diff --git a/release-notes.md b/release-notes.md new file mode 100644 index 00000000..91eabb40 --- /dev/null +++ b/release-notes.md @@ -0,0 +1,24 @@ +## Release notes +### 1.5 +* Added support for setting a custom game path globally. +* Added default GOG path on Mac. + +### 1.4 +* Fixed detection of non-default game paths on 32-bit Windows. +* Removed support for SilVerPLuM (discontinued). +* Removed support for overriding the target platform (no longer needed since SMAPI crossplatforms mods automatically). + +### 1.3 +* Added support for non-default game paths on Windows. + +### 1.2 +* Exclude game binaries from mod build output. + +### 1.1 +* Added support for overriding the target platform. + +### 1.0 +* Initial release. +* Added support for detecting the game path automatically. +* Added support for injecting XNA/MonoGame references automatically based on the OS. +* Added support for mod builders like SilVerPLuM. \ No newline at end of file From ffc54bed9c8fe063052306b1e1bc138b9848618d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 5 Jun 2017 17:27:49 -0400 Subject: [PATCH 032/186] add .gitignore and .gitattributes --- .gitattributes | 2 ++ .gitignore | 5 +++++ 2 files changed, 7 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..2c61027a --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# always normalise line endings +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..41bf35be --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# Visual Studio cache/options +.vs/ + +# NuGet packages +*.nupkg From 66b1b583e5233a4b50b832733d8e2cbc329a70be Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 5 Jun 2017 16:01:08 -0400 Subject: [PATCH 033/186] add error if a game folder is detected, but it doesn't contain Stardew Valley or SMAPI --- build/smapi.targets | 3 +++ release-notes.md | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/build/smapi.targets b/build/smapi.targets index de3420d1..21989bd1 100644 --- a/build/smapi.targets +++ b/build/smapi.targets @@ -101,5 +101,8 @@ + + + \ No newline at end of file diff --git a/release-notes.md b/release-notes.md index 91eabb40..6ec456bc 100644 --- a/release-notes.md +++ b/release-notes.md @@ -1,4 +1,7 @@ ## Release notes +### 1.6 +* Added a build error if a game folder is found, but doesn't contain Stardew Valley or SMAPI. + ### 1.5 * Added support for setting a custom game path globally. * Added default GOG path on Mac. @@ -21,4 +24,4 @@ * Initial release. * Added support for detecting the game path automatically. * Added support for injecting XNA/MonoGame references automatically based on the OS. -* Added support for mod builders like SilVerPLuM. \ No newline at end of file +* Added support for mod builders like SilVerPLuM. From ffc339bc29a9b4b08d53a5d147546630116d82d7 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 5 Jun 2017 15:24:53 -0400 Subject: [PATCH 034/186] add support for deploying mods to game folder automatically --- README.md | 46 +++++++++++++++++++++++---------------------- build/smapi.targets | 25 ++++++++++++++++++++++++ release-notes.md | 1 + 3 files changed, 50 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 351ebd91..115711f3 100644 --- a/README.md +++ b/README.md @@ -32,34 +32,36 @@ The package... 3. Compile on any platform. ## Simplify mod development -### Package your mod into the game directory automatically -During development, it's helpful to have the mod files packaged into your `Mods` directory automatically each time you build. To do that: +### Package your mod into the game folder automatically +You can copy your mod files into the `Mods` folder automatically each time you build, so you don't +need to do it manually: 1. Edit your mod's `.csproj` file. -2. Add this block of code at the bottom, right above the closing `` tag: +2. Add this block under the ` - - $(GamePath)\Mods\$(TargetName) - - - - - - - ``` -3. Optionally, edit the `` value to change the name, or add any additional files your mod needs. + ```xml + + $(MSBuildProjectName) + + ``` -That's it! Each time you build, the files in `\Mods\` will be updated. +That's it! Each time you build, the files in `\Mods\` will be updated with +your `manifest.json`, build output, and any `i18n` files. + +Notes: +* To add custom files, just [add them to the build output](https://stackoverflow.com/a/10828462/262123). +* To customise the folder name, just replace `$(MSBuildProjectName)` with the folder name you want. +* If your project references another mod, make sure the reference is [_not_ marked 'copy local'](https://msdn.microsoft.com/en-us/library/t1zz5y8c(v=vs.100).aspx). ### Debug into the mod code -Stepping into your mod code when the game is running is straightforward, since this package injects the configuration automatically. To do it: +Stepping into your mod code when the game is running is straightforward, since this package injects +the configuration automatically. To do it: -1. [Package your mod into the game directory automatically](#package-your-mod-into-the-game-directory-automatically). +1. [Package your mod into the game folder automatically](#package-your-mod-into-the-game-folder-automatically). 2. Launch the project with debugging in Visual Studio or MonoDevelop. -This will deploy your mod files into the game directory, launch SMAPI, and attach a debugger automatically. Now you can step through your code, set breakpoints, etc. +This will deploy your mod files into the game folder, launch SMAPI, and attach a debugger +automatically. Now you can step through your code, set breakpoints, etc. ## Troubleshoot ### "Failed to find the game install path" @@ -89,9 +91,6 @@ the game location yourself. There's two ways to do that: 4. Replace `PATH_HERE` with your custom game install path. -The configuration will check your custom path first, then fall back to the default paths (so it'll -still compile on a different computer). - * **Option 2: set the path in the project file.** _(You'll need to do it for every project that uses the package.)_ 1. Get the folder path containing the Stardew Valley `.exe` file. @@ -105,5 +104,8 @@ still compile on a different computer). 3. Replace `PATH_HERE` with your custom game install path. +The configuration will check your custom path first, then fall back to the default paths (so it'll +still compile on a different computer). + ## Versions See [release notes](release-notes.md). diff --git a/build/smapi.targets b/build/smapi.targets index 21989bd1..ea6af723 100644 --- a/build/smapi.targets +++ b/build/smapi.targets @@ -105,4 +105,29 @@ + + + + + + $(GamePath)\Mods\$(DeployModFolderName) + $(ProjectDir)\manifest.json + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/release-notes.md b/release-notes.md index 6ec456bc..ff2734f8 100644 --- a/release-notes.md +++ b/release-notes.md @@ -1,5 +1,6 @@ ## Release notes ### 1.6 +* Added support for deploying mod files into `Mods` automatically. * Added a build error if a game folder is found, but doesn't contain Stardew Valley or SMAPI. ### 1.5 From f74a0429eccf205ac52f51244a716bab786eab2a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 5 Jun 2017 17:46:45 -0400 Subject: [PATCH 035/186] update nuspec for release --- README.md | 1 - package.nuspec | 10 +++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 115711f3..c5e01c91 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,6 @@ The package... only_). * packages the mod automatically into the game's mod folder when you build the code (_optional_). - ## Contents * [Install](#install) * [Simplify mod development](#simplify-mod-development) diff --git a/package.nuspec b/package.nuspec index 5967c2e3..c68d57c6 100644 --- a/package.nuspec +++ b/package.nuspec @@ -2,16 +2,16 @@ Pathoschild.Stardew.ModBuildConfig - 1.5 + 1.6 MSBuild config for Stardew Valley mods Pathoschild Pathoschild false - https://github.com/Pathoschild/Stardew.ModBuildConfig/blob/1.5/LICENSE.txt - https://github.com/Pathoschild/Stardew.ModBuildConfig - https://raw.githubusercontent.com/Pathoschild/Stardew.ModBuildConfig/1.5/assets/nuget-icon.png + https://github.com/Pathoschild/Stardew.ModBuildConfig/blob/1.6/LICENSE.txt + https://github.com/Pathoschild/Stardew.ModBuildConfig#readme + https://raw.githubusercontent.com/Pathoschild/Stardew.ModBuildConfig/1.6/assets/nuget-icon.png Automates the build configuration for crossplatform Stardew Valley SMAPI mods. - Added support for setting a custom game path globally; added default GOG path on Mac. + SMAPI mods are now deployed automatically to your game folder if you set the 'DeployModFolderName' property; added a build error if a game folder is found, but it doesn't contain Stardew Valley or SMAPI. From cf6445b8e1cfac6299102582288f5406daa724a5 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 9 Jul 2017 23:29:51 -0400 Subject: [PATCH 036/186] improve OS and game path detection to avoid crossplatform issues --- build/smapi.targets | 58 ++++++++++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/build/smapi.targets b/build/smapi.targets index ea6af723..e2af5d90 100644 --- a/build/smapi.targets +++ b/build/smapi.targets @@ -6,35 +6,36 @@ - - Linux - Mac - Windows - + + + + Linux + $(HOME)/GOG Games/Stardew Valley/game + $(HOME)/.local/share/Steam/steamapps/common/Stardew Valley + + + + + Mac + /Applications/Stardew Valley.app/Contents/MacOS + $(HOME)/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS + + + + + Windows + C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley + C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley + $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\GOG.com\Games\1453375253', 'PATH', null, RegistryView.Registry32)) + $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150', 'InstallLocation', null, RegistryView.Registry64, RegistryView.Registry32)) + + + - - - $(HOME)/GOG Games/Stardew Valley/game - $(HOME)/.local/share/Steam/steamapps/common/Stardew Valley - - - /Applications/Stardew Valley.app/Contents/MacOS - $(HOME)/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS - - - C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley - C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley - $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\GOG.com\Games\1453375253', 'PATH', null, RegistryView.Registry32)) - $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150', 'InstallLocation', null, RegistryView.Registry64, RegistryView.Registry32)) - - - @@ -99,6 +100,9 @@ ## validate #######--> + + + @@ -107,7 +111,7 @@ From 4ca843ee90ed996be60da2b73c4c010335a07171 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 9 Jul 2017 23:30:48 -0400 Subject: [PATCH 037/186] update nuspec for release --- package.nuspec | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.nuspec b/package.nuspec index c68d57c6..900e2a5b 100644 --- a/package.nuspec +++ b/package.nuspec @@ -2,16 +2,16 @@ Pathoschild.Stardew.ModBuildConfig - 1.6 + 1.6.1 MSBuild config for Stardew Valley mods Pathoschild Pathoschild false - https://github.com/Pathoschild/Stardew.ModBuildConfig/blob/1.6/LICENSE.txt + https://github.com/Pathoschild/Stardew.ModBuildConfig/blob/1.6.1/LICENSE.txt https://github.com/Pathoschild/Stardew.ModBuildConfig#readme - https://raw.githubusercontent.com/Pathoschild/Stardew.ModBuildConfig/1.6/assets/nuget-icon.png + https://raw.githubusercontent.com/Pathoschild/Stardew.ModBuildConfig/1.6.1/assets/nuget-icon.png Automates the build configuration for crossplatform Stardew Valley SMAPI mods. - SMAPI mods are now deployed automatically to your game folder if you set the 'DeployModFolderName' property; added a build error if a game folder is found, but it doesn't contain Stardew Valley or SMAPI. + Improved OS and game path detection. From 9beefb65fa0213603c3aa52875d88c0b410dbfd6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 9 Jul 2017 23:33:08 -0400 Subject: [PATCH 038/186] clear GamePlatform if unknown --- build/smapi.targets | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/build/smapi.targets b/build/smapi.targets index e2af5d90..b2ce5967 100644 --- a/build/smapi.targets +++ b/build/smapi.targets @@ -32,6 +32,11 @@ $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150', 'InstallLocation', null, RegistryView.Registry64, RegistryView.Registry32)) + + + + + - + @@ -106,12 +98,12 @@ #######--> - + - - + + @@ -139,4 +131,4 @@ - \ No newline at end of file + From 1fa12092c6130fbf8fb9c256f6c23e69c740a841 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 10 Jul 2017 15:45:22 -0400 Subject: [PATCH 040/186] combine Linux/Mac paths Mono may report Mac as either 'Unix' or 'OSX', and we don't really need to distinguish them for our purposes anyway. --- build/smapi.targets | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/build/smapi.targets b/build/smapi.targets index 3a9c42c1..a544067b 100644 --- a/build/smapi.targets +++ b/build/smapi.targets @@ -9,14 +9,13 @@ ## find platform + game path #######--> - + + $(HOME)/GOG Games/Stardew Valley/game $(HOME)/.local/share/Steam/steamapps/common/Stardew Valley - - - - + + /Applications/Stardew Valley.app/Contents/MacOS $(HOME)/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS From 059c277623b1e1813094fe1a4ae80db432bce0ec Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 10 Jul 2017 15:46:20 -0400 Subject: [PATCH 041/186] update nuspec for release --- package.nuspec | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.nuspec b/package.nuspec index 900e2a5b..f216803a 100644 --- a/package.nuspec +++ b/package.nuspec @@ -2,16 +2,16 @@ Pathoschild.Stardew.ModBuildConfig - 1.6.1 + 1.6.2 MSBuild config for Stardew Valley mods Pathoschild Pathoschild false - https://github.com/Pathoschild/Stardew.ModBuildConfig/blob/1.6.1/LICENSE.txt + https://github.com/Pathoschild/Stardew.ModBuildConfig/blob/1.6.2/LICENSE.txt https://github.com/Pathoschild/Stardew.ModBuildConfig#readme - https://raw.githubusercontent.com/Pathoschild/Stardew.ModBuildConfig/1.6.1/assets/nuget-icon.png + https://raw.githubusercontent.com/Pathoschild/Stardew.ModBuildConfig/1.6.2/assets/nuget-icon.png Automates the build configuration for crossplatform Stardew Valley SMAPI mods. - Improved OS and game path detection. + Improved OS and game path detection; removed undocumented GamePlatform variable. From eca5e56883f3d94e0541710a10e4b2b44d1e38f7 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 27 Jul 2017 19:15:40 -0400 Subject: [PATCH 042/186] also reference Microsoft.Xna.Framework.Xact by default --- build/smapi.targets | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build/smapi.targets b/build/smapi.targets index a544067b..d95fa887 100644 --- a/build/smapi.targets +++ b/build/smapi.targets @@ -46,6 +46,9 @@ false + + false + $(GamePath)\Stardew Valley.exe false From 285c0448fd6f8c90fa05dc668dd27f452fa513ab Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 28 Jul 2017 00:10:32 -0400 Subject: [PATCH 043/186] add option to create release zip files (Windows-only) CodeTaskFactory doesn't seem to be available on Linux/Mac. --- README.md | 19 ++++++-- build/smapi.targets | 104 ++++++++++++++++++++++++++++++++++++-------- 2 files changed, 102 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index c5e01c91..3c137663 100644 --- a/README.md +++ b/README.md @@ -36,12 +36,10 @@ You can copy your mod files into the `Mods` folder automatically each time you b need to do it manually: 1. Edit your mod's `.csproj` file. -2. Add this block under the `` line: ```xml - - $(MSBuildProjectName) - + $(MSBuildProjectName) ``` That's it! Each time you build, the files in `\Mods\` will be updated with @@ -62,6 +60,19 @@ the configuration automatically. To do it: This will deploy your mod files into the game folder, launch SMAPI, and attach a debugger automatically. Now you can step through your code, set breakpoints, etc. +### Create release zips automatically (Windows-only) +You can create the mod package automatically when you build: + +1. Edit your mod's `.csproj` file. +2. Add this block above the first `` line: + + ```xml + $(SolutionDir)\_releases + ``` + +That's it! Each time you build, the mod files will be zipped into `_releases\.zip`. (You +can change the value to save the zips somewhere else.) + ## Troubleshoot ### "Failed to find the game install path" That error means the package couldn't figure out where the game is installed. You need to specify diff --git a/build/smapi.targets b/build/smapi.targets index d95fa887..b9f7e98e 100644 --- a/build/smapi.targets +++ b/build/smapi.targets @@ -1,6 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + @@ -30,9 +92,10 @@ - + + @@ -95,8 +158,12 @@ + + @@ -110,27 +177,30 @@ - - + + $(GamePath)\Mods\$(DeployModFolderName) - $(ProjectDir)\manifest.json + - + + - + - + - - - - - + + + + + + + From 4dd197068ec556d146e7e822b3a22b217c902a95 Mon Sep 17 00:00:00 2001 From: Mike Weaver Date: Thu, 27 Jul 2017 22:06:25 -0600 Subject: [PATCH 044/186] mark debugging support as Windows-only in readme (#4) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3c137663..96e4415d 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ Notes: * To customise the folder name, just replace `$(MSBuildProjectName)` with the folder name you want. * If your project references another mod, make sure the reference is [_not_ marked 'copy local'](https://msdn.microsoft.com/en-us/library/t1zz5y8c(v=vs.100).aspx). -### Debug into the mod code +### Debug into the mod code (Windows Only) Stepping into your mod code when the game is running is straightforward, since this package injects the configuration automatically. To do it: From f287ff616e471cf341f8b2cb726842816bdd8d29 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 28 Jul 2017 00:12:26 -0400 Subject: [PATCH 045/186] update nuspec --- README.md | 2 +- package.nuspec | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 96e4415d..c261e705 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ Notes: * To customise the folder name, just replace `$(MSBuildProjectName)` with the folder name you want. * If your project references another mod, make sure the reference is [_not_ marked 'copy local'](https://msdn.microsoft.com/en-us/library/t1zz5y8c(v=vs.100).aspx). -### Debug into the mod code (Windows Only) +### Debug into the mod code (Windows-only) Stepping into your mod code when the game is running is straightforward, since this package injects the configuration automatically. To do it: diff --git a/package.nuspec b/package.nuspec index f216803a..9abc7251 100644 --- a/package.nuspec +++ b/package.nuspec @@ -2,19 +2,19 @@ Pathoschild.Stardew.ModBuildConfig - 1.6.2 + 1.7 MSBuild config for Stardew Valley mods Pathoschild Pathoschild false - https://github.com/Pathoschild/Stardew.ModBuildConfig/blob/1.6.2/LICENSE.txt + https://github.com/Pathoschild/Stardew.ModBuildConfig/blob/1.7/LICENSE.txt https://github.com/Pathoschild/Stardew.ModBuildConfig#readme - https://raw.githubusercontent.com/Pathoschild/Stardew.ModBuildConfig/1.6.2/assets/nuget-icon.png + https://raw.githubusercontent.com/Pathoschild/Stardew.ModBuildConfig/1.7/assets/nuget-icon.png Automates the build configuration for crossplatform Stardew Valley SMAPI mods. - Improved OS and game path detection; removed undocumented GamePlatform variable. + Added option to create release zips on build; added reference to XNA's XACT library for audio-related mods. - + From a10e555095a4f7f0a6858149b3ffd6595fbd24e6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 28 Jul 2017 11:10:06 -0400 Subject: [PATCH 046/186] fix i18n folder being flattened --- build/smapi.targets | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/build/smapi.targets b/build/smapi.targets index b9f7e98e..ea0cfffb 100644 --- a/build/smapi.targets +++ b/build/smapi.targets @@ -34,9 +34,13 @@ { foreach (ITaskItem file in Files) { + // get file info string filePath = file.ItemSpec; string entryName = file.GetMetadata("RecursiveDir") + file.GetMetadata("Filename") + file.GetMetadata("Extension"); - + if (new FileInfo(filePath).Directory.Name.Equals("i18n", StringComparison.InvariantCultureIgnoreCase)) + entryName = Path.Combine("i18n", entryName); + + // add to zip using (Stream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read)) using (Stream fileStreamInZip = archive.CreateEntry(entryName).Open()) { @@ -187,8 +191,8 @@ - + @@ -198,9 +202,10 @@ + - + From 34eff4b08612f687c32faf8ec1b3363ba9fe0aa0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 28 Jul 2017 11:21:38 -0400 Subject: [PATCH 047/186] fix duplicate manifest/i18n files added to release zip if they're also copied to build output --- build/smapi.targets | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/build/smapi.targets b/build/smapi.targets index ea0cfffb..58737fba 100644 --- a/build/smapi.targets +++ b/build/smapi.targets @@ -190,9 +190,13 @@ - - - + + + + + + + From 048888e09347b38d19bbac5d1c123ec83acfb367 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 28 Jul 2017 11:26:08 -0400 Subject: [PATCH 048/186] update nuspec --- package.nuspec | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/package.nuspec b/package.nuspec index 9abc7251..b8e96481 100644 --- a/package.nuspec +++ b/package.nuspec @@ -2,16 +2,18 @@ Pathoschild.Stardew.ModBuildConfig - 1.7 + 1.7.1 MSBuild config for Stardew Valley mods Pathoschild Pathoschild false - https://github.com/Pathoschild/Stardew.ModBuildConfig/blob/1.7/LICENSE.txt + https://github.com/Pathoschild/Stardew.ModBuildConfig/blob/1.7.1/LICENSE.txt https://github.com/Pathoschild/Stardew.ModBuildConfig#readme - https://raw.githubusercontent.com/Pathoschild/Stardew.ModBuildConfig/1.7/assets/nuget-icon.png + https://raw.githubusercontent.com/Pathoschild/Stardew.ModBuildConfig/1.7.1/assets/nuget-icon.png Automates the build configuration for crossplatform Stardew Valley SMAPI mods. - Added option to create release zips on build; added reference to XNA's XACT library for audio-related mods. + + 1.7 added an option to create release zips on build and added a reference to XNA's XACT library for audio-related mods. + 1.7.1 fixed an issue where i18n folders were flattened, and ensures that the manifest/i18n files in the project take precedence over those in the build output if both are present. From 06ed54b2c6c2e1f49d6975a34ea36f685d7f7c49 Mon Sep 17 00:00:00 2001 From: James Stine Date: Sun, 13 Aug 2017 17:44:00 -0400 Subject: [PATCH 049/186] First commit. --- .gitignore | 235 +++++++++++++++++++++++++ Dewdrop.sln | 22 +++ Dewdrop/Controllers/CheckController.cs | 60 +++++++ Dewdrop/Dewdrop.csproj | 27 +++ Dewdrop/Models/IModModel.cs | 16 ++ Dewdrop/Models/ModGenericModel.cs | 40 +++++ Dewdrop/Models/NexusResponseModel.cs | 48 +++++ Dewdrop/Program.cs | 27 +++ Dewdrop/Properties/launchSettings.json | 29 +++ Dewdrop/Startup.cs | 43 +++++ Dewdrop/appsettings.Development.json | 10 ++ Dewdrop/appsettings.json | 8 + 12 files changed, 565 insertions(+) create mode 100644 .gitignore create mode 100644 Dewdrop.sln create mode 100644 Dewdrop/Controllers/CheckController.cs create mode 100644 Dewdrop/Dewdrop.csproj create mode 100644 Dewdrop/Models/IModModel.cs create mode 100644 Dewdrop/Models/ModGenericModel.cs create mode 100644 Dewdrop/Models/NexusResponseModel.cs create mode 100644 Dewdrop/Program.cs create mode 100644 Dewdrop/Properties/launchSettings.json create mode 100644 Dewdrop/Startup.cs create mode 100644 Dewdrop/appsettings.Development.json create mode 100644 Dewdrop/appsettings.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..500c9423 --- /dev/null +++ b/.gitignore @@ -0,0 +1,235 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Microsoft Azure ApplicationInsights config file +ApplicationInsights.config + +# Windows Store app package directory +AppPackages/ +BundleArtifacts/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe + +# FAKE - F# Make +.fake/ +Dewdrop/ScaffoldingReadMe.txt diff --git a/Dewdrop.sln b/Dewdrop.sln new file mode 100644 index 00000000..761e6ccc --- /dev/null +++ b/Dewdrop.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26430.16 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dewdrop", "Dewdrop\Dewdrop.csproj", "{0DEC7A86-4D43-43B0-A5F8-7686DDB6EC97}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0DEC7A86-4D43-43B0-A5F8-7686DDB6EC97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0DEC7A86-4D43-43B0-A5F8-7686DDB6EC97}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0DEC7A86-4D43-43B0-A5F8-7686DDB6EC97}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0DEC7A86-4D43-43B0-A5F8-7686DDB6EC97}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/Dewdrop/Controllers/CheckController.cs b/Dewdrop/Controllers/CheckController.cs new file mode 100644 index 00000000..f3cdd364 --- /dev/null +++ b/Dewdrop/Controllers/CheckController.cs @@ -0,0 +1,60 @@ +using System; +using System.Net.Http; +using Microsoft.AspNetCore.Mvc; +using System.Threading.Tasks; +using Newtonsoft.Json; +using System.Collections.Generic; +using Dewdrop.Models; + +namespace Dewdrop.Controllers +{ + [Produces("application/json")] + [Route("api/check")] + public class CheckController : Controller + { + [HttpPost] + public async Task Post([FromBody] NexusResponseModel[] mods) + { + using (var client = new HttpClient()) + { + // the return array of mods + var modList = new List(); + + foreach (var mod in mods) + { + try + { + // create request with HttpRequestMessage + var request = new HttpRequestMessage(HttpMethod.Get, new Uri($"http://www.nexusmods.com/stardewvalley/mods/{mod.Id}")); + + // add the Nexus Client useragent to get JSON response from the site + request.Headers.UserAgent.ParseAdd("Nexus Client v0.63.15"); + + // send the request out + var response = await client.SendAsync(request); + // ensure the response is valid (throws exception) + response.EnsureSuccessStatusCode(); + + // get the JSON string of the response + var stringResponse = await response.Content.ReadAsStringAsync(); + + // create the mod data from the JSON string + var modData = JsonConvert.DeserializeObject(stringResponse); + + // add to the list of mods + modList.Add(modData.ModInfo()); + } + catch (Exception ex) + { + var modData = mod.ModInfo(); + modData.Valid = false; + + modList.Add(modData); + } + } + + return JsonConvert.SerializeObject(modList, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); + } + } + } +} \ No newline at end of file diff --git a/Dewdrop/Dewdrop.csproj b/Dewdrop/Dewdrop.csproj new file mode 100644 index 00000000..fa1d88eb --- /dev/null +++ b/Dewdrop/Dewdrop.csproj @@ -0,0 +1,27 @@ + + + + netcoreapp1.1 + portable-net45+win8 + + + + + + + + + + + + + + + + + + + + + + diff --git a/Dewdrop/Models/IModModel.cs b/Dewdrop/Models/IModModel.cs new file mode 100644 index 00000000..aa6583d4 --- /dev/null +++ b/Dewdrop/Models/IModModel.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Dewdrop.Models +{ + interface IModModel + { + /// + /// Basic information in the form of + /// + /// + ModGenericModel ModInfo(); + } +} diff --git a/Dewdrop/Models/ModGenericModel.cs b/Dewdrop/Models/ModGenericModel.cs new file mode 100644 index 00000000..829c396a --- /dev/null +++ b/Dewdrop/Models/ModGenericModel.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Dewdrop.Models +{ + public class ModGenericModel + { + /// + /// An identifier for the mod. + /// + public int Id { get; set; } + + /// + /// The mod's name. + /// + public string Name { get; set; } + + /// + /// The vendor identifier for the mod. + /// + public string Vendor { get; set; } + + /// + /// The mod's version number. + /// + public string Version { get; set; } + + /// + /// The mod's URL + /// + public string Url { get; set; } + + /// + /// Is the mod a valid mod. + /// + public bool Valid { get; set; } = true; + } +} diff --git a/Dewdrop/Models/NexusResponseModel.cs b/Dewdrop/Models/NexusResponseModel.cs new file mode 100644 index 00000000..e954a8bc --- /dev/null +++ b/Dewdrop/Models/NexusResponseModel.cs @@ -0,0 +1,48 @@ +using System; +using Newtonsoft.Json; + +namespace Dewdrop.Models +{ + public class NexusResponseModel : IModModel + { + /// + /// The name of the mod. + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// The version of the mod. + /// + [JsonProperty("version")] + public string Version { get; set; } + + /// + /// The NexusMod ID for the mod. + /// + [JsonProperty("id")] + public int Id { get; set; } + + /// + /// The URL of the mod. + /// + [JsonProperty("mod_page_uri")] + public string Url { get; set; } + + /// + /// Return mod information about a Nexus mod + /// + /// + public ModGenericModel ModInfo() + { + return new ModGenericModel + { + Id = Id, + Version = Version, + Name = Name, + Url = Url, + Vendor = "Nexus" + }; + } + } +} diff --git a/Dewdrop/Program.cs b/Dewdrop/Program.cs new file mode 100644 index 00000000..c6a5a642 --- /dev/null +++ b/Dewdrop/Program.cs @@ -0,0 +1,27 @@ +using System; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace Dewdrop +{ + public class Program + { + public static void Main(string[] args) + { + var host = new WebHostBuilder() + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseStartup() + .UseApplicationInsights() + .Build(); + + host.Run(); + } + } +} diff --git a/Dewdrop/Properties/launchSettings.json b/Dewdrop/Properties/launchSettings.json new file mode 100644 index 00000000..c15134dc --- /dev/null +++ b/Dewdrop/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:59482/", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "api/check", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Dewdrop": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "api/check", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:59483" + } + } +} diff --git a/Dewdrop/Startup.cs b/Dewdrop/Startup.cs new file mode 100644 index 00000000..00c18bab --- /dev/null +++ b/Dewdrop/Startup.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Dewdrop +{ + public class Startup + { + public Startup(IHostingEnvironment env) + { + var builder = new ConfigurationBuilder() + .SetBasePath(env.ContentRootPath) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) + .AddEnvironmentVariables(); + Configuration = builder.Build(); + } + + public IConfigurationRoot Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + // Add framework services. + services.AddMvc(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + { + loggerFactory.AddConsole(Configuration.GetSection("Logging")); + loggerFactory.AddDebug(); + + app.UseMvc(); + } + } +} diff --git a/Dewdrop/appsettings.Development.json b/Dewdrop/appsettings.Development.json new file mode 100644 index 00000000..fa8ce71a --- /dev/null +++ b/Dewdrop/appsettings.Development.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/Dewdrop/appsettings.json b/Dewdrop/appsettings.json new file mode 100644 index 00000000..5fff67ba --- /dev/null +++ b/Dewdrop/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Warning" + } + } +} From 8c5bd12f4793a9c866b6046c05482592a5c799bc Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 19 Sep 2017 22:45:28 -0400 Subject: [PATCH 050/186] merge assembly rewriters into main SMAPI project (#347) --- README.md | 2 - .../Properties/AssemblyInfo.cs | 7 -- ...StardewModdingAPI.AssemblyRewriters.csproj | 73 ------------------- .../packages.config | 4 - .../InteractiveInstaller.cs | 6 +- src/StardewModdingAPI.sln | 14 +--- src/StardewModdingAPI/Constants.cs | 8 +- .../Framework/ModLoading/AssemblyLoader.cs | 4 +- .../ModLoading}/Finders/EventFinder.cs | 6 +- .../ModLoading}/Finders/FieldFinder.cs | 4 +- .../ModLoading}/Finders/MethodFinder.cs | 6 +- .../ModLoading}/Finders/PropertyFinder.cs | 4 +- .../ModLoading}/Finders/TypeFinder.cs | 4 +- .../ModLoading}/IInstructionRewriter.cs | 6 +- .../IncompatibleInstructionException.cs | 6 +- .../ModLoading/InvalidModStateException.cs | 4 +- .../Framework/ModLoading/ModMetadataStatus.cs | 4 +- .../Framework/ModLoading}/Platform.cs | 6 +- .../ModLoading}/PlatformAssemblyMap.cs | 4 +- .../Framework/ModLoading}/RewriteHelper.cs | 2 +- .../Rewriters/FieldReplaceRewriter.cs | 6 +- .../Rewriters/FieldToPropertyRewriter.cs | 6 +- .../Rewriters/MethodParentRewriter.cs | 4 +- .../Rewriters/TypeReferenceRewriter.cs | 6 +- .../Rewriters/Wrappers/SpriteBatchWrapper.cs | 6 +- .../Framework/SContentManager.cs | 2 +- src/StardewModdingAPI/Program.cs | 1 - .../StardewModdingAPI.csproj | 22 ++++-- src/common.targets | 1 - src/prepare-install-package.targets | 2 - 30 files changed, 68 insertions(+), 162 deletions(-) delete mode 100644 src/StardewModdingAPI.AssemblyRewriters/Properties/AssemblyInfo.cs delete mode 100644 src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj delete mode 100644 src/StardewModdingAPI.AssemblyRewriters/packages.config rename src/{StardewModdingAPI.AssemblyRewriters => StardewModdingAPI/Framework/ModLoading}/Finders/EventFinder.cs (96%) rename src/{StardewModdingAPI.AssemblyRewriters => StardewModdingAPI/Framework/ModLoading}/Finders/FieldFinder.cs (97%) rename src/{StardewModdingAPI.AssemblyRewriters => StardewModdingAPI/Framework/ModLoading}/Finders/MethodFinder.cs (96%) rename src/{StardewModdingAPI.AssemblyRewriters => StardewModdingAPI/Framework/ModLoading}/Finders/PropertyFinder.cs (97%) rename src/{StardewModdingAPI.AssemblyRewriters => StardewModdingAPI/Framework/ModLoading}/Finders/TypeFinder.cs (98%) rename src/{StardewModdingAPI.AssemblyRewriters => StardewModdingAPI/Framework/ModLoading}/IInstructionRewriter.cs (94%) rename src/{StardewModdingAPI.AssemblyRewriters => StardewModdingAPI/Framework/ModLoading}/IncompatibleInstructionException.cs (90%) rename src/{StardewModdingAPI.AssemblyRewriters => StardewModdingAPI/Framework/ModLoading}/Platform.cs (74%) rename src/{StardewModdingAPI.AssemblyRewriters => StardewModdingAPI/Framework/ModLoading}/PlatformAssemblyMap.cs (96%) rename src/{StardewModdingAPI.AssemblyRewriters => StardewModdingAPI/Framework/ModLoading}/RewriteHelper.cs (98%) rename src/{StardewModdingAPI.AssemblyRewriters => StardewModdingAPI/Framework/ModLoading}/Rewriters/FieldReplaceRewriter.cs (93%) rename src/{StardewModdingAPI.AssemblyRewriters => StardewModdingAPI/Framework/ModLoading}/Rewriters/FieldToPropertyRewriter.cs (93%) rename src/{StardewModdingAPI.AssemblyRewriters => StardewModdingAPI/Framework/ModLoading}/Rewriters/MethodParentRewriter.cs (97%) rename src/{StardewModdingAPI.AssemblyRewriters => StardewModdingAPI/Framework/ModLoading}/Rewriters/TypeReferenceRewriter.cs (97%) rename src/{StardewModdingAPI.AssemblyRewriters => StardewModdingAPI/Framework/ModLoading}/Rewriters/Wrappers/SpriteBatchWrapper.cs (95%) diff --git a/README.md b/README.md index cbf0ec36..ebc2dd57 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,6 @@ on the wiki for the first-time setup. Mono.Cecil.dll Newtonsoft.Json.dll StardewModdingAPI - StardewModdingAPI.AssemblyRewriters.dll StardewModdingAPI.config.json StardewModdingAPI.exe StardewModdingAPI.pdb @@ -123,7 +122,6 @@ on the wiki for the first-time setup. Mods/* Mono.Cecil.dll Newtonsoft.Json.dll - StardewModdingAPI.AssemblyRewriters.dll StardewModdingAPI.config.json StardewModdingAPI.exe StardewModdingAPI.pdb diff --git a/src/StardewModdingAPI.AssemblyRewriters/Properties/AssemblyInfo.cs b/src/StardewModdingAPI.AssemblyRewriters/Properties/AssemblyInfo.cs deleted file mode 100644 index 25fbfef9..00000000 --- a/src/StardewModdingAPI.AssemblyRewriters/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,7 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -[assembly: AssemblyTitle("StardewModdingAPI.AssemblyRewriters")] -[assembly: AssemblyDescription("Contains internal SMAPI code for converting mods between Linux/Mac and Windows. This assembly is used by SMAPI internally and shouldn't be referenced directly by mods.")] -[assembly: AssemblyProduct("StardewModdingAPI.CrossplatformRewriters")] -[assembly: Guid("10db0676-9fc1-4771-a2c8-e2519f091e49")] diff --git a/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj b/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj deleted file mode 100644 index 8416bd51..00000000 --- a/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj +++ /dev/null @@ -1,73 +0,0 @@ - - - - - Debug - x86 - {10DB0676-9FC1-4771-A2C8-E2519F091E49} - Library - Properties - StardewModdingAPI.AssemblyRewriters - StardewModdingAPI.AssemblyRewriters - v4.5 - 512 - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.dll - True - - - ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Mdb.dll - True - - - ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Pdb.dll - True - - - - - - Properties\GlobalAssemblyInfo.cs - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/StardewModdingAPI.AssemblyRewriters/packages.config b/src/StardewModdingAPI.AssemblyRewriters/packages.config deleted file mode 100644 index 88fbc79d..00000000 --- a/src/StardewModdingAPI.AssemblyRewriters/packages.config +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/StardewModdingAPI.Installer/InteractiveInstaller.cs b/src/StardewModdingAPI.Installer/InteractiveInstaller.cs index 01288f33..b52529a6 100644 --- a/src/StardewModdingAPI.Installer/InteractiveInstaller.cs +++ b/src/StardewModdingAPI.Installer/InteractiveInstaller.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -81,7 +81,6 @@ namespace StardewModdingApi.Installer yield return GetInstallPath("StardewModdingAPI.exe"); yield return GetInstallPath("StardewModdingAPI.config.json"); yield return GetInstallPath("StardewModdingAPI.data.json"); - yield return GetInstallPath("StardewModdingAPI.AssemblyRewriters.dll"); yield return GetInstallPath("System.ValueTuple.dll"); yield return GetInstallPath("steam_appid.txt"); @@ -98,7 +97,8 @@ namespace StardewModdingApi.Installer // obsolete yield return GetInstallPath("Mods/.cache"); // 1.3-1.4 yield return GetInstallPath("Mono.Cecil.Rocks.dll"); // 1.3–1.8 - yield return GetInstallPath("StardewModdingAPI-settings.json"); // 1.0-1.4 + yield return GetInstallPath("StardewModdingAPI-settings.json"); // 1.0–1.4 + yield return GetInstallPath("StardewModdingAPI.AssemblyRewriters.dll"); // 1.3–1.15.4 if (modsDir.Exists) { foreach (DirectoryInfo modDir in modsDir.EnumerateDirectories()) diff --git a/src/StardewModdingAPI.sln b/src/StardewModdingAPI.sln index 9c3f18f8..a2e0ec44 100644 --- a/src/StardewModdingAPI.sln +++ b/src/StardewModdingAPI.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26430.16 +VisualStudioVersion = 15.0.26730.15 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrainerMod", "TrainerMod\TrainerMod.csproj", "{28480467-1A48-46A7-99F8-236D95225359}" EndProject @@ -27,8 +27,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Installer {F1A573B0-F436-472C-AE29-0B91EA6B9F8F} = {F1A573B0-F436-472C-AE29-0B91EA6B9F8F} EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.AssemblyRewriters", "StardewModdingAPI.AssemblyRewriters\StardewModdingAPI.AssemblyRewriters.csproj", "{10DB0676-9FC1-4771-A2C8-E2519F091E49}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Tests", "StardewModdingAPI.Tests\StardewModdingAPI.Tests.csproj", "{36CCB19E-92EB-48C7-9615-98EEFD45109B}" EndProject Global @@ -71,16 +69,6 @@ Global {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|Mixed Platforms.Build.0 = Release|x86 {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|x86.ActiveCfg = Release|x86 {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|x86.Build.0 = Release|x86 - {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Debug|Any CPU.ActiveCfg = Debug|x86 - {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 - {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Debug|Mixed Platforms.Build.0 = Debug|x86 - {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Debug|x86.ActiveCfg = Debug|x86 - {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Debug|x86.Build.0 = Debug|x86 - {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Release|Any CPU.ActiveCfg = Release|x86 - {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Release|Mixed Platforms.ActiveCfg = Release|x86 - {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Release|Mixed Platforms.Build.0 = Release|x86 - {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Release|x86.ActiveCfg = Release|x86 - {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Release|x86.Build.0 = Release|x86 {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|Any CPU.ActiveCfg = Debug|x86 {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|Mixed Platforms.Build.0 = Debug|x86 diff --git a/src/StardewModdingAPI/Constants.cs b/src/StardewModdingAPI/Constants.cs index fea9377a..4bbd7328 100644 --- a/src/StardewModdingAPI/Constants.cs +++ b/src/StardewModdingAPI/Constants.cs @@ -4,12 +4,12 @@ using System.IO; using System.Linq; using System.Reflection; using Microsoft.Xna.Framework.Graphics; -using StardewModdingAPI.AssemblyRewriters; -using StardewModdingAPI.AssemblyRewriters.Finders; -using StardewModdingAPI.AssemblyRewriters.Rewriters; -using StardewModdingAPI.AssemblyRewriters.Rewriters.Wrappers; using StardewModdingAPI.Events; using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.ModLoading; +using StardewModdingAPI.Framework.ModLoading.Finders; +using StardewModdingAPI.Framework.ModLoading.Rewriters; +using StardewModdingAPI.Framework.ModLoading.Rewriters.Wrappers; using StardewValley; namespace StardewModdingAPI diff --git a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs index 9c642bef..27a81038 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs @@ -5,7 +5,6 @@ using System.Linq; using System.Reflection; using Mono.Cecil; using Mono.Cecil.Cil; -using StardewModdingAPI.AssemblyRewriters; using StardewModdingAPI.Framework.Exceptions; namespace StardewModdingAPI.Framework.ModLoading @@ -270,8 +269,7 @@ namespace StardewModdingAPI.Framework.ModLoading return; // get assembly - Assembly assembly; - if (!this.TypeAssemblies.TryGetValue(type.FullName, out assembly)) + if (!this.TypeAssemblies.TryGetValue(type.FullName, out Assembly assembly)) return; // replace scope diff --git a/src/StardewModdingAPI.AssemblyRewriters/Finders/EventFinder.cs b/src/StardewModdingAPI/Framework/ModLoading/Finders/EventFinder.cs similarity index 96% rename from src/StardewModdingAPI.AssemblyRewriters/Finders/EventFinder.cs rename to src/StardewModdingAPI/Framework/ModLoading/Finders/EventFinder.cs index c0051469..ce234e39 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/Finders/EventFinder.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/Finders/EventFinder.cs @@ -1,10 +1,10 @@ -using Mono.Cecil; +using Mono.Cecil; using Mono.Cecil.Cil; -namespace StardewModdingAPI.AssemblyRewriters.Finders +namespace StardewModdingAPI.Framework.ModLoading.Finders { /// Finds incompatible CIL instructions that reference a given event and throws an . - public class EventFinder : IInstructionRewriter + internal class EventFinder : IInstructionRewriter { /********* ** Properties diff --git a/src/StardewModdingAPI.AssemblyRewriters/Finders/FieldFinder.cs b/src/StardewModdingAPI/Framework/ModLoading/Finders/FieldFinder.cs similarity index 97% rename from src/StardewModdingAPI.AssemblyRewriters/Finders/FieldFinder.cs rename to src/StardewModdingAPI/Framework/ModLoading/Finders/FieldFinder.cs index b44883e9..2feaf2e6 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/Finders/FieldFinder.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/Finders/FieldFinder.cs @@ -1,10 +1,10 @@ using Mono.Cecil; using Mono.Cecil.Cil; -namespace StardewModdingAPI.AssemblyRewriters.Finders +namespace StardewModdingAPI.Framework.ModLoading.Finders { /// Finds incompatible CIL instructions that reference a given field and throws an . - public class FieldFinder : IInstructionRewriter + internal class FieldFinder : IInstructionRewriter { /********* ** Properties diff --git a/src/StardewModdingAPI.AssemblyRewriters/Finders/MethodFinder.cs b/src/StardewModdingAPI/Framework/ModLoading/Finders/MethodFinder.cs similarity index 96% rename from src/StardewModdingAPI.AssemblyRewriters/Finders/MethodFinder.cs rename to src/StardewModdingAPI/Framework/ModLoading/Finders/MethodFinder.cs index 19dda58a..c3bb36e3 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/Finders/MethodFinder.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/Finders/MethodFinder.cs @@ -1,10 +1,10 @@ -using Mono.Cecil; +using Mono.Cecil; using Mono.Cecil.Cil; -namespace StardewModdingAPI.AssemblyRewriters.Finders +namespace StardewModdingAPI.Framework.ModLoading.Finders { /// Finds incompatible CIL instructions that reference a given method and throws an . - public class MethodFinder : IInstructionRewriter + internal class MethodFinder : IInstructionRewriter { /********* ** Properties diff --git a/src/StardewModdingAPI.AssemblyRewriters/Finders/PropertyFinder.cs b/src/StardewModdingAPI/Framework/ModLoading/Finders/PropertyFinder.cs similarity index 97% rename from src/StardewModdingAPI.AssemblyRewriters/Finders/PropertyFinder.cs rename to src/StardewModdingAPI/Framework/ModLoading/Finders/PropertyFinder.cs index 441f15f2..d1fed84b 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/Finders/PropertyFinder.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/Finders/PropertyFinder.cs @@ -1,10 +1,10 @@ using Mono.Cecil; using Mono.Cecil.Cil; -namespace StardewModdingAPI.AssemblyRewriters.Finders +namespace StardewModdingAPI.Framework.ModLoading.Finders { /// Finds incompatible CIL instructions that reference a given property and throws an . - public class PropertyFinder : IInstructionRewriter + internal class PropertyFinder : IInstructionRewriter { /********* ** Properties diff --git a/src/StardewModdingAPI.AssemblyRewriters/Finders/TypeFinder.cs b/src/StardewModdingAPI/Framework/ModLoading/Finders/TypeFinder.cs similarity index 98% rename from src/StardewModdingAPI.AssemblyRewriters/Finders/TypeFinder.cs rename to src/StardewModdingAPI/Framework/ModLoading/Finders/TypeFinder.cs index 0560e38e..e67e6766 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/Finders/TypeFinder.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/Finders/TypeFinder.cs @@ -2,10 +2,10 @@ using System.Linq; using Mono.Cecil; using Mono.Cecil.Cil; -namespace StardewModdingAPI.AssemblyRewriters.Finders +namespace StardewModdingAPI.Framework.ModLoading.Finders { /// Finds incompatible CIL instructions that reference a given type and throws an . - public class TypeFinder : IInstructionRewriter + internal class TypeFinder : IInstructionRewriter { /********* ** Accessors diff --git a/src/StardewModdingAPI.AssemblyRewriters/IInstructionRewriter.cs b/src/StardewModdingAPI/Framework/ModLoading/IInstructionRewriter.cs similarity index 94% rename from src/StardewModdingAPI.AssemblyRewriters/IInstructionRewriter.cs rename to src/StardewModdingAPI/Framework/ModLoading/IInstructionRewriter.cs index 2f16b23d..9b35cdae 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/IInstructionRewriter.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/IInstructionRewriter.cs @@ -1,10 +1,10 @@ -using Mono.Cecil; +using Mono.Cecil; using Mono.Cecil.Cil; -namespace StardewModdingAPI.AssemblyRewriters +namespace StardewModdingAPI.Framework.ModLoading { /// Rewrites CIL instructions for compatibility. - public interface IInstructionRewriter + internal interface IInstructionRewriter { /********* ** Accessors diff --git a/src/StardewModdingAPI.AssemblyRewriters/IncompatibleInstructionException.cs b/src/StardewModdingAPI/Framework/ModLoading/IncompatibleInstructionException.cs similarity index 90% rename from src/StardewModdingAPI.AssemblyRewriters/IncompatibleInstructionException.cs rename to src/StardewModdingAPI/Framework/ModLoading/IncompatibleInstructionException.cs index f7e6bd8f..17ec24b1 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/IncompatibleInstructionException.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/IncompatibleInstructionException.cs @@ -1,9 +1,9 @@ -using System; +using System; -namespace StardewModdingAPI.AssemblyRewriters +namespace StardewModdingAPI.Framework.ModLoading { /// An exception raised when an incompatible instruction is found while loading a mod assembly. - public class IncompatibleInstructionException : Exception + internal class IncompatibleInstructionException : Exception { /********* ** Accessors diff --git a/src/StardewModdingAPI/Framework/ModLoading/InvalidModStateException.cs b/src/StardewModdingAPI/Framework/ModLoading/InvalidModStateException.cs index ab11272a..075e237a 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/InvalidModStateException.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/InvalidModStateException.cs @@ -1,9 +1,9 @@ -using System; +using System; namespace StardewModdingAPI.Framework.ModLoading { /// An exception which indicates that something went seriously wrong while loading mods, and SMAPI should abort outright. - public class InvalidModStateException : Exception + internal class InvalidModStateException : Exception { /// Construct an instance. /// The error message. diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModMetadataStatus.cs b/src/StardewModdingAPI/Framework/ModLoading/ModMetadataStatus.cs index 1b2b0b55..ab65f7b4 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/ModMetadataStatus.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/ModMetadataStatus.cs @@ -1,4 +1,4 @@ -namespace StardewModdingAPI.Framework.ModLoading +namespace StardewModdingAPI.Framework.ModLoading { /// Indicates the status of a mod's metadata resolution. internal enum ModMetadataStatus @@ -9,4 +9,4 @@ /// The mod cannot be loaded. Failed } -} \ No newline at end of file +} diff --git a/src/StardewModdingAPI.AssemblyRewriters/Platform.cs b/src/StardewModdingAPI/Framework/ModLoading/Platform.cs similarity index 74% rename from src/StardewModdingAPI.AssemblyRewriters/Platform.cs rename to src/StardewModdingAPI/Framework/ModLoading/Platform.cs index 8888a9a8..45e881c4 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/Platform.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/Platform.cs @@ -1,7 +1,7 @@ -namespace StardewModdingAPI.AssemblyRewriters +namespace StardewModdingAPI.Framework.ModLoading { /// The game's platform version. - public enum Platform + internal enum Platform { /// The Linux/Mac version of the game. Mono, @@ -9,4 +9,4 @@ namespace StardewModdingAPI.AssemblyRewriters /// The Windows version of the game. Windows } -} \ No newline at end of file +} diff --git a/src/StardewModdingAPI.AssemblyRewriters/PlatformAssemblyMap.cs b/src/StardewModdingAPI/Framework/ModLoading/PlatformAssemblyMap.cs similarity index 96% rename from src/StardewModdingAPI.AssemblyRewriters/PlatformAssemblyMap.cs rename to src/StardewModdingAPI/Framework/ModLoading/PlatformAssemblyMap.cs index fce2b187..463f45e8 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/PlatformAssemblyMap.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/PlatformAssemblyMap.cs @@ -3,10 +3,10 @@ using System.Linq; using System.Reflection; using Mono.Cecil; -namespace StardewModdingAPI.AssemblyRewriters +namespace StardewModdingAPI.Framework.ModLoading { /// Metadata for mapping assemblies to the current . - public class PlatformAssemblyMap + internal class PlatformAssemblyMap { /********* ** Accessors diff --git a/src/StardewModdingAPI.AssemblyRewriters/RewriteHelper.cs b/src/StardewModdingAPI/Framework/ModLoading/RewriteHelper.cs similarity index 98% rename from src/StardewModdingAPI.AssemblyRewriters/RewriteHelper.cs rename to src/StardewModdingAPI/Framework/ModLoading/RewriteHelper.cs index cfb330dd..56a60a72 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/RewriteHelper.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/RewriteHelper.cs @@ -4,7 +4,7 @@ using System.Reflection; using Mono.Cecil; using Mono.Cecil.Cil; -namespace StardewModdingAPI.AssemblyRewriters +namespace StardewModdingAPI.Framework.ModLoading { /// Provides helper methods for field rewriters. internal static class RewriteHelper diff --git a/src/StardewModdingAPI.AssemblyRewriters/Rewriters/FieldReplaceRewriter.cs b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs similarity index 93% rename from src/StardewModdingAPI.AssemblyRewriters/Rewriters/FieldReplaceRewriter.cs rename to src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs index 73844073..7b838f13 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/Rewriters/FieldReplaceRewriter.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs @@ -2,12 +2,12 @@ using System; using System.Reflection; using Mono.Cecil; using Mono.Cecil.Cil; -using StardewModdingAPI.AssemblyRewriters.Finders; +using StardewModdingAPI.Framework.ModLoading.Finders; -namespace StardewModdingAPI.AssemblyRewriters.Rewriters +namespace StardewModdingAPI.Framework.ModLoading.Rewriters { /// Rewrites references to one field with another. - public class FieldReplaceRewriter : FieldFinder + internal class FieldReplaceRewriter : FieldFinder { /********* ** Properties diff --git a/src/StardewModdingAPI.AssemblyRewriters/Rewriters/FieldToPropertyRewriter.cs b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs similarity index 93% rename from src/StardewModdingAPI.AssemblyRewriters/Rewriters/FieldToPropertyRewriter.cs rename to src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs index 3f57042d..8ef14103 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/Rewriters/FieldToPropertyRewriter.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs @@ -1,12 +1,12 @@ using System; using Mono.Cecil; using Mono.Cecil.Cil; -using StardewModdingAPI.AssemblyRewriters.Finders; +using StardewModdingAPI.Framework.ModLoading.Finders; -namespace StardewModdingAPI.AssemblyRewriters.Rewriters +namespace StardewModdingAPI.Framework.ModLoading.Rewriters { /// Rewrites field references into property references. - public class FieldToPropertyRewriter : FieldFinder + internal class FieldToPropertyRewriter : FieldFinder { /********* ** Properties diff --git a/src/StardewModdingAPI.AssemblyRewriters/Rewriters/MethodParentRewriter.cs b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs similarity index 97% rename from src/StardewModdingAPI.AssemblyRewriters/Rewriters/MethodParentRewriter.cs rename to src/StardewModdingAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs index 035ef211..1b95b83b 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/Rewriters/MethodParentRewriter.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs @@ -2,10 +2,10 @@ using System; using Mono.Cecil; using Mono.Cecil.Cil; -namespace StardewModdingAPI.AssemblyRewriters.Rewriters +namespace StardewModdingAPI.Framework.ModLoading.Rewriters { /// Rewrites method references from one parent type to another if the signatures match. - public class MethodParentRewriter : IInstructionRewriter + internal class MethodParentRewriter : IInstructionRewriter { /********* ** Properties diff --git a/src/StardewModdingAPI.AssemblyRewriters/Rewriters/TypeReferenceRewriter.cs b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs similarity index 97% rename from src/StardewModdingAPI.AssemblyRewriters/Rewriters/TypeReferenceRewriter.cs rename to src/StardewModdingAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs index da6d9bc9..2c444b64 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/Rewriters/TypeReferenceRewriter.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs @@ -1,12 +1,12 @@ using System; using Mono.Cecil; using Mono.Cecil.Cil; -using StardewModdingAPI.AssemblyRewriters.Finders; +using StardewModdingAPI.Framework.ModLoading.Finders; -namespace StardewModdingAPI.AssemblyRewriters.Rewriters +namespace StardewModdingAPI.Framework.ModLoading.Rewriters { /// Rewrites all references to a type. - public class TypeReferenceRewriter : TypeFinder + internal class TypeReferenceRewriter : TypeFinder { /********* ** Properties diff --git a/src/StardewModdingAPI.AssemblyRewriters/Rewriters/Wrappers/SpriteBatchWrapper.cs b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/Wrappers/SpriteBatchWrapper.cs similarity index 95% rename from src/StardewModdingAPI.AssemblyRewriters/Rewriters/Wrappers/SpriteBatchWrapper.cs rename to src/StardewModdingAPI/Framework/ModLoading/Rewriters/Wrappers/SpriteBatchWrapper.cs index ee68f1d5..7a631f69 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/Rewriters/Wrappers/SpriteBatchWrapper.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/Wrappers/SpriteBatchWrapper.cs @@ -2,10 +2,10 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; -namespace StardewModdingAPI.AssemblyRewriters.Rewriters.Wrappers +namespace StardewModdingAPI.Framework.ModLoading.Rewriters.Wrappers { /// Wraps methods that are incompatible when converting compiled code between MonoGame and XNA. - public class SpriteBatchWrapper : SpriteBatch + internal class SpriteBatchWrapper : SpriteBatch { /********* ** Public methods @@ -56,4 +56,4 @@ namespace StardewModdingAPI.AssemblyRewriters.Rewriters.Wrappers base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect, transformMatrix); } } -} \ No newline at end of file +} diff --git a/src/StardewModdingAPI/Framework/SContentManager.cs b/src/StardewModdingAPI/Framework/SContentManager.cs index 9553e79f..33403841 100644 --- a/src/StardewModdingAPI/Framework/SContentManager.cs +++ b/src/StardewModdingAPI/Framework/SContentManager.cs @@ -6,8 +6,8 @@ using System.Linq; using System.Reflection; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; -using StardewModdingAPI.AssemblyRewriters; using StardewModdingAPI.Framework.Content; +using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Utilities; using StardewModdingAPI.Metadata; diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index ad873598..c1165216 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -12,7 +12,6 @@ using System.Management; using System.Windows.Forms; #endif using Newtonsoft.Json; -using StardewModdingAPI.AssemblyRewriters; using StardewModdingAPI.Events; using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.Exceptions; diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 8daf21b7..5f59d125 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -91,9 +91,25 @@ Properties\GlobalAssemblyInfo.cs + + + + + + + + + + + + + + + + @@ -257,12 +273,6 @@ false - - - {10db0676-9fc1-4771-a2c8-e2519f091e49} - StardewModdingAPI.AssemblyRewriters - - \ No newline at end of file diff --git a/src/common.targets b/src/common.targets index ee138524..c450b3a5 100644 --- a/src/common.targets +++ b/src/common.targets @@ -83,7 +83,6 @@ - diff --git a/src/prepare-install-package.targets b/src/prepare-install-package.targets index f2a2b23c..44997fef 100644 --- a/src/prepare-install-package.targets +++ b/src/prepare-install-package.targets @@ -27,7 +27,6 @@ - @@ -41,7 +40,6 @@ - From 954de8c4f2683b39a3f5a11fd29c39130bc31bdb Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 19 Sep 2017 22:46:00 -0400 Subject: [PATCH 051/186] move monitor.LogOnce into internal extensions for reuse (#347) --- .../Framework/InternalExtensions.cs | 16 ++++++++++++- .../Framework/ModLoading/AssemblyLoader.cs | 24 ++++--------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/StardewModdingAPI/Framework/InternalExtensions.cs b/src/StardewModdingAPI/Framework/InternalExtensions.cs index 2842bc83..3709e05d 100644 --- a/src/StardewModdingAPI/Framework/InternalExtensions.cs +++ b/src/StardewModdingAPI/Framework/InternalExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -71,6 +71,20 @@ namespace StardewModdingAPI.Framework } } + /// Log a message for the player or developer the first time it occurs. + /// The monitor through which to log the message. + /// The hash of logged messages. + /// The message to log. + /// The log severity level. + public static void LogOnce(this IMonitor monitor, HashSet hash, string message, LogLevel level = LogLevel.Trace) + { + if (!hash.Contains(message)) + { + monitor.Log(message, level); + hash.Add(message); + } + } + /**** ** Exceptions ****/ diff --git a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs index 27a81038..01dc602a 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs @@ -191,7 +191,7 @@ namespace StardewModdingAPI.Framework.ModLoading // remove old assembly reference if (this.AssemblyMap.RemoveNames.Any(name => module.AssemblyReferences[i].Name == name)) { - this.LogOnce(this.Monitor, loggedMessages, $"{logPrefix}Rewriting {filename} for OS..."); + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Rewriting {filename} for OS..."); platformChanged = true; module.AssemblyReferences.RemoveAt(i); i--; @@ -221,7 +221,7 @@ namespace StardewModdingAPI.Framework.ModLoading { if (rewriter.Rewrite(module, method, this.AssemblyMap, platformChanged)) { - this.LogOnce(this.Monitor, loggedMessages, $"{logPrefix}Rewrote {filename} to fix {rewriter.NounPhrase}..."); + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Rewrote {filename} to fix {rewriter.NounPhrase}..."); anyRewritten = true; } } @@ -229,7 +229,7 @@ namespace StardewModdingAPI.Framework.ModLoading { if (!assumeCompatible) throw new IncompatibleInstructionException(rewriter.NounPhrase, $"Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {filename}."); - this.LogOnce(this.Monitor, loggedMessages, $"{logPrefix}Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {filename}, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly.", LogLevel.Warn); + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {filename}, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly.", LogLevel.Warn); } } @@ -243,7 +243,7 @@ namespace StardewModdingAPI.Framework.ModLoading { if (rewriter.Rewrite(module, cil, instruction, this.AssemblyMap, platformChanged)) { - this.LogOnce(this.Monitor, loggedMessages, $"{logPrefix}Rewrote {filename} to fix {rewriter.NounPhrase}..."); + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Rewrote {filename} to fix {rewriter.NounPhrase}..."); anyRewritten = true; } } @@ -251,7 +251,7 @@ namespace StardewModdingAPI.Framework.ModLoading { if (!assumeCompatible) throw new IncompatibleInstructionException(rewriter.NounPhrase, $"Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {filename}."); - this.LogOnce(this.Monitor, loggedMessages, $"{logPrefix}Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {filename}, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly.", LogLevel.Warn); + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {filename}, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly.", LogLevel.Warn); } } } @@ -289,19 +289,5 @@ namespace StardewModdingAPI.Framework.ModLoading select method ); } - - /// Log a message for the player or developer the first time it occurs. - /// The monitor through which to log the message. - /// The hash of logged messages. - /// The message to log. - /// The log severity level. - private void LogOnce(IMonitor monitor, HashSet hash, string message, LogLevel level = LogLevel.Trace) - { - if (!hash.Contains(message)) - { - monitor.Log(message, level); - hash.Add(message); - } - } } } From c513bb011c858e59abbd77f75080f4a1d8b712f9 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 19 Sep 2017 22:52:52 -0400 Subject: [PATCH 052/186] pass mod metadata into rewriters (#347) --- .../Framework/ModLoading/AssemblyLoader.cs | 14 ++++++++------ .../Framework/ModLoading/Finders/EventFinder.cs | 6 ++++-- .../Framework/ModLoading/Finders/FieldFinder.cs | 6 ++++-- .../Framework/ModLoading/Finders/MethodFinder.cs | 6 ++++-- .../Framework/ModLoading/Finders/PropertyFinder.cs | 6 ++++-- .../Framework/ModLoading/Finders/TypeFinder.cs | 6 ++++-- .../Framework/ModLoading/IInstructionRewriter.cs | 6 ++++-- .../ModLoading/Rewriters/FieldReplaceRewriter.cs | 3 ++- .../Rewriters/FieldToPropertyRewriter.cs | 3 ++- .../ModLoading/Rewriters/MethodParentRewriter.cs | 6 ++++-- .../ModLoading/Rewriters/TypeReferenceRewriter.cs | 6 ++++-- src/StardewModdingAPI/Program.cs | 2 +- 12 files changed, 45 insertions(+), 25 deletions(-) diff --git a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs index 01dc602a..f9c43f1f 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs @@ -53,11 +53,12 @@ namespace StardewModdingAPI.Framework.ModLoading } /// Preprocess and load an assembly. + /// The mod for which the assembly is being loaded. /// The assembly file path. /// Assume the mod is compatible, even if incompatible code is detected. /// Returns the rewrite metadata for the preprocessed assembly. /// An incompatible CIL instruction was found while rewriting the assembly. - public Assembly Load(string assemblyPath, bool assumeCompatible) + public Assembly Load(IModMetadata mod, string assemblyPath, bool assumeCompatible) { // get referenced local assemblies AssemblyParseResult[] assemblies; @@ -86,7 +87,7 @@ namespace StardewModdingAPI.Framework.ModLoading if (assembly.Status == AssemblyLoadStatus.AlreadyLoaded) continue; - bool changed = this.RewriteAssembly(assembly.Definition, assumeCompatible, logPrefix: " "); + bool changed = this.RewriteAssembly(mod, assembly.Definition, assumeCompatible, logPrefix: " "); if (changed) { if (!oneAssembly) @@ -173,12 +174,13 @@ namespace StardewModdingAPI.Framework.ModLoading ** Assembly rewriting ****/ /// Rewrite the types referenced by an assembly. + /// The mod for which the assembly is being loaded. /// The assembly to rewrite. /// Assume the mod is compatible, even if incompatible code is detected. /// A string to prefix to log messages. /// Returns whether the assembly was modified. /// An incompatible CIL instruction was found while rewriting the assembly. - private bool RewriteAssembly(AssemblyDefinition assembly, bool assumeCompatible, string logPrefix) + private bool RewriteAssembly(IModMetadata mod, AssemblyDefinition assembly, bool assumeCompatible, string logPrefix) { ModuleDefinition module = assembly.MainModule; HashSet loggedMessages = new HashSet(); @@ -211,7 +213,7 @@ namespace StardewModdingAPI.Framework.ModLoading // find (and optionally rewrite) incompatible instructions bool anyRewritten = false; - IInstructionRewriter[] rewriters = Constants.GetRewriters().ToArray(); + IInstructionRewriter[] rewriters = Constants.GetRewriters(this.Monitor).ToArray(); foreach (MethodDefinition method in this.GetMethods(module)) { // check method definition @@ -219,7 +221,7 @@ namespace StardewModdingAPI.Framework.ModLoading { try { - if (rewriter.Rewrite(module, method, this.AssemblyMap, platformChanged)) + if (rewriter.Rewrite(mod, module, method, this.AssemblyMap, platformChanged)) { this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Rewrote {filename} to fix {rewriter.NounPhrase}..."); anyRewritten = true; @@ -241,7 +243,7 @@ namespace StardewModdingAPI.Framework.ModLoading { try { - if (rewriter.Rewrite(module, cil, instruction, this.AssemblyMap, platformChanged)) + if (rewriter.Rewrite(mod, module, cil, instruction, this.AssemblyMap, platformChanged)) { this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Rewrote {filename} to fix {rewriter.NounPhrase}..."); anyRewritten = true; diff --git a/src/StardewModdingAPI/Framework/ModLoading/Finders/EventFinder.cs b/src/StardewModdingAPI/Framework/ModLoading/Finders/EventFinder.cs index ce234e39..ac5034c4 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/Finders/EventFinder.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/Finders/EventFinder.cs @@ -38,18 +38,20 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders } /// Rewrite a method definition for compatibility. + /// The mod to which the module belongs. /// The module being rewritten. /// The method definition to rewrite. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. /// Returns whether the instruction was rewritten. /// The CIL instruction is not compatible, and can't be rewritten. - public virtual bool Rewrite(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + public virtual bool Rewrite(IModMetadata mod, ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) { return false; } /// Rewrite a CIL instruction for compatibility. + /// The mod to which the module belongs. /// The module being rewritten. /// The CIL rewriter. /// The instruction to rewrite. @@ -57,7 +59,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders /// Whether the mod was compiled on a different platform. /// Returns whether the instruction was rewritten. /// The CIL instruction is not compatible, and can't be rewritten. - public virtual bool Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + public virtual bool Rewrite(IModMetadata mod, ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { if (!this.IsMatch(instruction)) return false; diff --git a/src/StardewModdingAPI/Framework/ModLoading/Finders/FieldFinder.cs b/src/StardewModdingAPI/Framework/ModLoading/Finders/FieldFinder.cs index 2feaf2e6..008399d5 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/Finders/FieldFinder.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/Finders/FieldFinder.cs @@ -38,18 +38,20 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders } /// Rewrite a method definition for compatibility. + /// The mod to which the module belongs. /// The module being rewritten. /// The method definition to rewrite. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. /// Returns whether the instruction was rewritten. /// The CIL instruction is not compatible, and can't be rewritten. - public virtual bool Rewrite(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + public virtual bool Rewrite(IModMetadata mod, ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) { return false; } /// Rewrite a CIL instruction for compatibility. + /// The mod to which the module belongs. /// The module being rewritten. /// The CIL rewriter. /// The instruction to rewrite. @@ -57,7 +59,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders /// Whether the mod was compiled on a different platform. /// Returns whether the instruction was rewritten. /// The CIL instruction is not compatible, and can't be rewritten. - public virtual bool Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + public virtual bool Rewrite(IModMetadata mod, ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { if (!this.IsMatch(instruction)) return false; diff --git a/src/StardewModdingAPI/Framework/ModLoading/Finders/MethodFinder.cs b/src/StardewModdingAPI/Framework/ModLoading/Finders/MethodFinder.cs index c3bb36e3..2a6dc99e 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/Finders/MethodFinder.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/Finders/MethodFinder.cs @@ -38,18 +38,20 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders } /// Rewrite a method definition for compatibility. + /// The mod to which the module belongs. /// The module being rewritten. /// The method definition to rewrite. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. /// Returns whether the instruction was rewritten. /// The CIL instruction is not compatible, and can't be rewritten. - public virtual bool Rewrite(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + public virtual bool Rewrite(IModMetadata mod, ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) { return false; } /// Rewrite a CIL instruction for compatibility. + /// The mod to which the module belongs. /// The module being rewritten. /// The CIL rewriter. /// The instruction to rewrite. @@ -57,7 +59,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders /// Whether the mod was compiled on a different platform. /// Returns whether the instruction was rewritten. /// The CIL instruction is not compatible, and can't be rewritten. - public bool Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + public bool Rewrite(IModMetadata mod, ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { if (!this.IsMatch(instruction)) return false; diff --git a/src/StardewModdingAPI/Framework/ModLoading/Finders/PropertyFinder.cs b/src/StardewModdingAPI/Framework/ModLoading/Finders/PropertyFinder.cs index d1fed84b..a0ce1cbf 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/Finders/PropertyFinder.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/Finders/PropertyFinder.cs @@ -38,18 +38,20 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders } /// Rewrite a method definition for compatibility. + /// The mod to which the module belongs. /// The module being rewritten. /// The method definition to rewrite. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. /// Returns whether the instruction was rewritten. /// The CIL instruction is not compatible, and can't be rewritten. - public virtual bool Rewrite(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + public virtual bool Rewrite(IModMetadata mod, ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) { return false; } /// Rewrite a CIL instruction for compatibility. + /// The mod to which the module belongs. /// The module being rewritten. /// The CIL rewriter. /// The instruction to rewrite. @@ -57,7 +59,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders /// Whether the mod was compiled on a different platform. /// Returns whether the instruction was rewritten. /// The CIL instruction is not compatible, and can't be rewritten. - public virtual bool Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + public virtual bool Rewrite(IModMetadata mod, ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { if (!this.IsMatch(instruction)) return false; diff --git a/src/StardewModdingAPI/Framework/ModLoading/Finders/TypeFinder.cs b/src/StardewModdingAPI/Framework/ModLoading/Finders/TypeFinder.cs index e67e6766..a3005c85 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/Finders/TypeFinder.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/Finders/TypeFinder.cs @@ -34,13 +34,14 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders } /// Rewrite a method definition for compatibility. + /// The mod to which the module belongs. /// The module being rewritten. /// The method definition to rewrite. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. /// Returns whether the instruction was rewritten. /// The CIL instruction is not compatible, and can't be rewritten. - public virtual bool Rewrite(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + public virtual bool Rewrite(IModMetadata mod, ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) { if (!this.IsMatch(method)) return false; @@ -49,6 +50,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders } /// Rewrite a CIL instruction for compatibility. + /// The mod to which the module belongs. /// The module being rewritten. /// The CIL rewriter. /// The instruction to rewrite. @@ -56,7 +58,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders /// Whether the mod was compiled on a different platform. /// Returns whether the instruction was rewritten. /// The CIL instruction is not compatible, and can't be rewritten. - public virtual bool Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + public virtual bool Rewrite(IModMetadata mod, ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { if (!this.IsMatch(instruction)) return false; diff --git a/src/StardewModdingAPI/Framework/ModLoading/IInstructionRewriter.cs b/src/StardewModdingAPI/Framework/ModLoading/IInstructionRewriter.cs index 9b35cdae..ce12c717 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/IInstructionRewriter.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/IInstructionRewriter.cs @@ -17,15 +17,17 @@ namespace StardewModdingAPI.Framework.ModLoading ** Methods *********/ /// Rewrite a method definition for compatibility. + /// The mod to which the module belongs. /// The module being rewritten. /// The method definition to rewrite. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. /// Returns whether the instruction was rewritten. /// The CIL instruction is not compatible, and can't be rewritten. - bool Rewrite(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged); + bool Rewrite(IModMetadata mod, ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged); /// Rewrite a CIL instruction for compatibility. + /// The mod to which the module belongs. /// The module being rewritten. /// The CIL rewriter. /// The instruction to rewrite. @@ -33,6 +35,6 @@ namespace StardewModdingAPI.Framework.ModLoading /// Whether the mod was compiled on a different platform. /// Returns whether the instruction was rewritten. /// The CIL instruction is not compatible, and can't be rewritten. - bool Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged); + bool Rewrite(IModMetadata mod, ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged); } } diff --git a/src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs index 7b838f13..fb2a9a96 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs @@ -33,6 +33,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters } /// Rewrite a CIL instruction for compatibility. + /// The mod to which the module belongs. /// The module being rewritten. /// The CIL rewriter. /// The instruction to rewrite. @@ -40,7 +41,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters /// Whether the mod was compiled on a different platform. /// Returns whether the instruction was rewritten. /// The CIL instruction is not compatible, and can't be rewritten. - public override bool Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + public override bool Rewrite(IModMetadata mod, ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { if (!this.IsMatch(instruction)) return false; diff --git a/src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs index 8ef14103..03d1f707 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs @@ -33,6 +33,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters } /// Rewrite a CIL instruction for compatibility. + /// The mod to which the module belongs. /// The module being rewritten. /// The CIL rewriter. /// The instruction to rewrite. @@ -40,7 +41,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters /// Whether the mod was compiled on a different platform. /// Returns whether the instruction was rewritten. /// The CIL instruction is not compatible, and can't be rewritten. - public override bool Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + public override bool Rewrite(IModMetadata mod, ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { if (!this.IsMatch(instruction)) return false; diff --git a/src/StardewModdingAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs index 1b95b83b..1e116e1f 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs @@ -44,18 +44,20 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters } /// Rewrite a method definition for compatibility. + /// The mod to which the module belongs. /// The module being rewritten. /// The method definition to rewrite. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. /// Returns whether the instruction was rewritten. /// The CIL instruction is not compatible, and can't be rewritten. - public bool Rewrite(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + public bool Rewrite(IModMetadata mod, ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) { return false; } /// Rewrite a CIL instruction for compatibility. + /// The mod to which the module belongs. /// The module being rewritten. /// The CIL rewriter. /// The instruction to rewrite. @@ -63,7 +65,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters /// Whether the mod was compiled on a different platform. /// Returns whether the instruction was rewritten. /// The CIL instruction is not compatible, and can't be rewritten. - public bool Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + public bool Rewrite(IModMetadata mod, ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { if (!this.IsMatch(instruction, platformChanged)) return false; diff --git a/src/StardewModdingAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs index 2c444b64..8db39cfe 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs @@ -33,13 +33,14 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters } /// Rewrite a method definition for compatibility. + /// The mod to which the module belongs. /// The module being rewritten. /// The method definition to rewrite. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. /// Returns whether the instruction was rewritten. /// The CIL instruction is not compatible, and can't be rewritten. - public override bool Rewrite(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + public override bool Rewrite(IModMetadata mod, ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) { bool rewritten = false; @@ -87,6 +88,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters } /// Rewrite a CIL instruction for compatibility. + /// The mod to which the module belongs. /// The module being rewritten. /// The CIL rewriter. /// The instruction to rewrite. @@ -94,7 +96,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters /// Whether the mod was compiled on a different platform. /// Returns whether the instruction was rewritten. /// The CIL instruction is not compatible, and can't be rewritten. - public override bool Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + public override bool Rewrite(IModMetadata mod, ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { if (!this.IsMatch(instruction) && !instruction.ToString().Contains(this.FromTypeName)) return false; diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index c1165216..84af2777 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -645,7 +645,7 @@ namespace StardewModdingAPI Assembly modAssembly; try { - modAssembly = modAssemblyLoader.Load(assemblyPath, assumeCompatible: metadata.Compatibility?.Compatibility == ModCompatibilityType.AssumeCompatible); + modAssembly = modAssemblyLoader.Load(metadata, assemblyPath, assumeCompatible: metadata.Compatibility?.Compatibility == ModCompatibilityType.AssumeCompatible); } catch (IncompatibleInstructionException ex) { From fd10cf958cb68a41e1aa7ac4d7d2371205be0c75 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 19 Sep 2017 23:02:00 -0400 Subject: [PATCH 053/186] move rewriters into metadata class (#347) --- src/StardewModdingAPI/Constants.cs | 79 ---------------- .../Framework/ModLoading/AssemblyLoader.cs | 3 +- .../Metadata/InstructionMetadata.cs | 91 +++++++++++++++++++ .../StardewModdingAPI.csproj | 1 + 4 files changed, 94 insertions(+), 80 deletions(-) create mode 100644 src/StardewModdingAPI/Metadata/InstructionMetadata.cs diff --git a/src/StardewModdingAPI/Constants.cs b/src/StardewModdingAPI/Constants.cs index 4bbd7328..07647d2d 100644 --- a/src/StardewModdingAPI/Constants.cs +++ b/src/StardewModdingAPI/Constants.cs @@ -1,15 +1,9 @@ using System; -using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; -using Microsoft.Xna.Framework.Graphics; -using StardewModdingAPI.Events; using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.ModLoading; -using StardewModdingAPI.Framework.ModLoading.Finders; -using StardewModdingAPI.Framework.ModLoading.Rewriters; -using StardewModdingAPI.Framework.ModLoading.Rewriters.Wrappers; using StardewValley; namespace StardewModdingAPI @@ -147,79 +141,6 @@ namespace StardewModdingAPI return new PlatformAssemblyMap(targetPlatform, removeAssemblyReferences, targetAssemblies); } - /// Get rewriters which detect or fix incompatible CIL instructions in mod assemblies. - internal static IEnumerable GetRewriters() - { - return new IInstructionRewriter[] - { - /**** - ** Finders throw an exception when incompatible code is found. - ****/ - // changes in Stardew Valley 1.2 (with no rewriters) - new FieldFinder("StardewValley.Item", "set_Name"), - - // APIs removed in SMAPI 1.9 - new TypeFinder("StardewModdingAPI.Advanced.ConfigFile"), - new TypeFinder("StardewModdingAPI.Advanced.IConfigFile"), - new TypeFinder("StardewModdingAPI.Entities.SPlayer"), - new TypeFinder("StardewModdingAPI.Extensions"), - new TypeFinder("StardewModdingAPI.Inheritance.SGame"), - new TypeFinder("StardewModdingAPI.Inheritance.SObject"), - new TypeFinder("StardewModdingAPI.LogWriter"), - new TypeFinder("StardewModdingAPI.Manifest"), - new TypeFinder("StardewModdingAPI.Version"), - new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "DrawDebug"), - new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "DrawTick"), - new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPostRenderHudEventNoCheck"), - new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPostRenderGuiEventNoCheck"), - new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPreRenderHudEventNoCheck"), - new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPreRenderGuiEventNoCheck"), - - // APIs removed in SMAPI 2.0 -#if !SMAPI_1_x - new TypeFinder("StardewModdingAPI.Command"), - new TypeFinder("StardewModdingAPI.Config"), - new TypeFinder("StardewModdingAPI.Log"), - new EventFinder("StardewModdingAPI.Events.GameEvents", "Initialize"), - new EventFinder("StardewModdingAPI.Events.GameEvents", "LoadContent"), - new EventFinder("StardewModdingAPI.Events.GameEvents", "GameLoaded"), - new EventFinder("StardewModdingAPI.Events.GameEvents", "FirstUpdateTick"), - new EventFinder("StardewModdingAPI.Events.PlayerEvents", "LoadedGame"), - new EventFinder("StardewModdingAPI.Events.PlayerEvents", "FarmerChanged"), - new EventFinder("StardewModdingAPI.Events.TimeEvents", "DayOfMonthChanged"), - new EventFinder("StardewModdingAPI.Events.TimeEvents", "YearOfGameChanged"), - new EventFinder("StardewModdingAPI.Events.TimeEvents", "SeasonOfYearChanged"), - new EventFinder("StardewModdingAPI.Events.TimeEvents", "OnNewDay"), - new TypeFinder("StardewModdingAPI.Events.EventArgsCommand"), - new TypeFinder("StardewModdingAPI.Events.EventArgsFarmerChanged"), - new TypeFinder("StardewModdingAPI.Events.EventArgsLoadedGameChanged"), - new TypeFinder("StardewModdingAPI.Events.EventArgsNewDay"), - new TypeFinder("StardewModdingAPI.Events.EventArgsStringChanged"), - new PropertyFinder("StardewModdingAPI.Mod", "PathOnDisk"), - new PropertyFinder("StardewModdingAPI.Mod", "BaseConfigPath"), - new PropertyFinder("StardewModdingAPI.Mod", "PerSaveConfigFolder"), - new PropertyFinder("StardewModdingAPI.Mod", "PerSaveConfigPath"), -#endif - - /**** - ** Rewriters change CIL as needed to fix incompatible code - ****/ - // crossplatform - new MethodParentRewriter(typeof(SpriteBatch), typeof(SpriteBatchWrapper), onlyIfPlatformChanged: true), - - // Stardew Valley 1.2 - new FieldToPropertyRewriter(typeof(Game1), nameof(Game1.activeClickableMenu)), - new FieldToPropertyRewriter(typeof(Game1), nameof(Game1.currentMinigame)), - new FieldToPropertyRewriter(typeof(Game1), nameof(Game1.gameMode)), - new FieldToPropertyRewriter(typeof(Game1), nameof(Game1.player)), - new FieldReplaceRewriter(typeof(Game1), "borderFont", nameof(Game1.smallFont)), - new FieldReplaceRewriter(typeof(Game1), "smoothFont", nameof(Game1.smallFont)), - - // SMAPI 1.9 - new TypeReferenceRewriter("StardewModdingAPI.Inheritance.ItemStackChange", typeof(ItemStackChange)) - }; - } - /********* ** Private methods diff --git a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs index f9c43f1f..871a081f 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs @@ -6,6 +6,7 @@ using System.Reflection; using Mono.Cecil; using Mono.Cecil.Cil; using StardewModdingAPI.Framework.Exceptions; +using StardewModdingAPI.Metadata; namespace StardewModdingAPI.Framework.ModLoading { @@ -213,7 +214,7 @@ namespace StardewModdingAPI.Framework.ModLoading // find (and optionally rewrite) incompatible instructions bool anyRewritten = false; - IInstructionRewriter[] rewriters = Constants.GetRewriters(this.Monitor).ToArray(); + IInstructionRewriter[] rewriters = new InstructionMetadata().GetRewriters().ToArray(); foreach (MethodDefinition method in this.GetMethods(module)) { // check method definition diff --git a/src/StardewModdingAPI/Metadata/InstructionMetadata.cs b/src/StardewModdingAPI/Metadata/InstructionMetadata.cs new file mode 100644 index 00000000..67fdc041 --- /dev/null +++ b/src/StardewModdingAPI/Metadata/InstructionMetadata.cs @@ -0,0 +1,91 @@ +using System.Collections.Generic; +using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI.Events; +using StardewModdingAPI.Framework.ModLoading; +using StardewModdingAPI.Framework.ModLoading.Finders; +using StardewModdingAPI.Framework.ModLoading.Rewriters; +using StardewModdingAPI.Framework.ModLoading.Rewriters.Wrappers; +using StardewValley; + +namespace StardewModdingAPI.Metadata +{ + /// Provides CIL instruction handlers which rewrite mods for compatibility and throw exceptions for incompatible code. + internal class InstructionMetadata + { + /********* + ** Public methods + *********/ + /// Get rewriters which detect or fix incompatible CIL instructions in mod assemblies. + public IEnumerable GetRewriters() + { + return new IInstructionRewriter[] + { + /**** + ** Finders throw an exception when incompatible code is found. + ****/ + // changes in Stardew Valley 1.2 (with no rewriters) + new FieldFinder("StardewValley.Item", "set_Name"), + + // APIs removed in SMAPI 1.9 + new TypeFinder("StardewModdingAPI.Advanced.ConfigFile"), + new TypeFinder("StardewModdingAPI.Advanced.IConfigFile"), + new TypeFinder("StardewModdingAPI.Entities.SPlayer"), + new TypeFinder("StardewModdingAPI.Extensions"), + new TypeFinder("StardewModdingAPI.Inheritance.SGame"), + new TypeFinder("StardewModdingAPI.Inheritance.SObject"), + new TypeFinder("StardewModdingAPI.LogWriter"), + new TypeFinder("StardewModdingAPI.Manifest"), + new TypeFinder("StardewModdingAPI.Version"), + new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "DrawDebug"), + new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "DrawTick"), + new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPostRenderHudEventNoCheck"), + new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPostRenderGuiEventNoCheck"), + new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPreRenderHudEventNoCheck"), + new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPreRenderGuiEventNoCheck"), + + // APIs removed in SMAPI 2.0 +#if !SMAPI_1_x + new TypeFinder("StardewModdingAPI.Command"), + new TypeFinder("StardewModdingAPI.Config"), + new TypeFinder("StardewModdingAPI.Log"), + new EventFinder("StardewModdingAPI.Events.GameEvents", "Initialize"), + new EventFinder("StardewModdingAPI.Events.GameEvents", "LoadContent"), + new EventFinder("StardewModdingAPI.Events.GameEvents", "GameLoaded"), + new EventFinder("StardewModdingAPI.Events.GameEvents", "FirstUpdateTick"), + new EventFinder("StardewModdingAPI.Events.PlayerEvents", "LoadedGame"), + new EventFinder("StardewModdingAPI.Events.PlayerEvents", "FarmerChanged"), + new EventFinder("StardewModdingAPI.Events.TimeEvents", "DayOfMonthChanged"), + new EventFinder("StardewModdingAPI.Events.TimeEvents", "YearOfGameChanged"), + new EventFinder("StardewModdingAPI.Events.TimeEvents", "SeasonOfYearChanged"), + new EventFinder("StardewModdingAPI.Events.TimeEvents", "OnNewDay"), + new TypeFinder("StardewModdingAPI.Events.EventArgsCommand"), + new TypeFinder("StardewModdingAPI.Events.EventArgsFarmerChanged"), + new TypeFinder("StardewModdingAPI.Events.EventArgsLoadedGameChanged"), + new TypeFinder("StardewModdingAPI.Events.EventArgsNewDay"), + new TypeFinder("StardewModdingAPI.Events.EventArgsStringChanged"), + new PropertyFinder("StardewModdingAPI.Mod", "PathOnDisk"), + new PropertyFinder("StardewModdingAPI.Mod", "BaseConfigPath"), + new PropertyFinder("StardewModdingAPI.Mod", "PerSaveConfigFolder"), + new PropertyFinder("StardewModdingAPI.Mod", "PerSaveConfigPath"), +#endif + + /**** + ** Rewriters change CIL as needed to fix incompatible code + ****/ + // crossplatform + new MethodParentRewriter(typeof(SpriteBatch), typeof(SpriteBatchWrapper), onlyIfPlatformChanged: true), + + // Stardew Valley 1.2 + new FieldToPropertyRewriter(typeof(Game1), nameof(Game1.activeClickableMenu)), + new FieldToPropertyRewriter(typeof(Game1), nameof(Game1.currentMinigame)), + new FieldToPropertyRewriter(typeof(Game1), nameof(Game1.gameMode)), + new FieldToPropertyRewriter(typeof(Game1), nameof(Game1.player)), + new FieldReplaceRewriter(typeof(Game1), "borderFont", nameof(Game1.smallFont)), + new FieldReplaceRewriter(typeof(Game1), "smoothFont", nameof(Game1.smallFont)), + + // SMAPI 1.9 + new TypeReferenceRewriter("StardewModdingAPI.Inheritance.ItemStackChange", typeof(ItemStackChange)) + }; + } + } +} diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 5f59d125..d4b1cb69 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -226,6 +226,7 @@ + From ebf22c1b06cd12ce02df16278062f7d37bc66a4c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 19 Sep 2017 23:42:54 -0400 Subject: [PATCH 054/186] generalise IInstructionRewriter into IInstructionHandler (#347) --- .../Framework/ModLoading/AssemblyLoader.cs | 60 +++++++------ .../ModLoading/Finders/EventFinder.cs | 49 ++++++----- .../ModLoading/Finders/FieldFinder.cs | 49 ++++++----- .../ModLoading/Finders/MethodFinder.cs | 49 ++++++----- .../ModLoading/Finders/PropertyFinder.cs | 49 ++++++----- .../ModLoading/Finders/TypeFinder.cs | 54 ++++++------ .../ModLoading/IInstructionRewriter.cs | 32 ++++--- .../ModLoading/InstructionHandleResult.cs | 15 ++++ .../Rewriters/FieldReplaceRewriter.cs | 23 +++-- .../Rewriters/FieldToPropertyRewriter.cs | 23 +++-- .../Rewriters/MethodParentRewriter.cs | 39 ++++----- .../Rewriters/TypeReferenceRewriter.cs | 41 +++++---- .../Metadata/InstructionMetadata.cs | 84 +++++++++---------- .../StardewModdingAPI.csproj | 1 + 14 files changed, 286 insertions(+), 282 deletions(-) create mode 100644 src/StardewModdingAPI/Framework/ModLoading/InstructionHandleResult.cs diff --git a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs index 871a081f..3a8e4fa9 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs @@ -214,25 +214,31 @@ namespace StardewModdingAPI.Framework.ModLoading // find (and optionally rewrite) incompatible instructions bool anyRewritten = false; - IInstructionRewriter[] rewriters = new InstructionMetadata().GetRewriters().ToArray(); + IInstructionHandler[] handlers = new InstructionMetadata().GetHandlers().ToArray(); foreach (MethodDefinition method in this.GetMethods(module)) { // check method definition - foreach (IInstructionRewriter rewriter in rewriters) + foreach (IInstructionHandler handler in handlers) { - try + InstructionHandleResult result = handler.Handle(mod, module, method, this.AssemblyMap, platformChanged); + switch (result) { - if (rewriter.Rewrite(mod, module, method, this.AssemblyMap, platformChanged)) - { - this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Rewrote {filename} to fix {rewriter.NounPhrase}..."); + case InstructionHandleResult.Rewritten: + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Rewrote {filename} to fix {handler.NounPhrase}..."); anyRewritten = true; - } - } - catch (IncompatibleInstructionException) - { - if (!assumeCompatible) - throw new IncompatibleInstructionException(rewriter.NounPhrase, $"Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {filename}."); - this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {filename}, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly.", LogLevel.Warn); + break; + + case InstructionHandleResult.NotCompatible: + if (!assumeCompatible) + throw new IncompatibleInstructionException(handler.NounPhrase, $"Found an incompatible CIL instruction ({handler.NounPhrase}) while loading assembly {filename}."); + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Found an incompatible CIL instruction ({handler.NounPhrase}) while loading assembly {filename}, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly.", LogLevel.Warn); + break; + + case InstructionHandleResult.None: + break; + + default: + throw new NotSupportedException($"Unrecognised instruction handler result '{result}'."); } } @@ -240,21 +246,27 @@ namespace StardewModdingAPI.Framework.ModLoading ILProcessor cil = method.Body.GetILProcessor(); foreach (Instruction instruction in cil.Body.Instructions.ToArray()) { - foreach (IInstructionRewriter rewriter in rewriters) + foreach (IInstructionHandler rewriter in handlers) { - try + InstructionHandleResult result = rewriter.Handle(mod, module, cil, instruction, this.AssemblyMap, platformChanged); + switch (result) { - if (rewriter.Rewrite(mod, module, cil, instruction, this.AssemblyMap, platformChanged)) - { + case InstructionHandleResult.Rewritten: this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Rewrote {filename} to fix {rewriter.NounPhrase}..."); anyRewritten = true; - } - } - catch (IncompatibleInstructionException) - { - if (!assumeCompatible) - throw new IncompatibleInstructionException(rewriter.NounPhrase, $"Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {filename}."); - this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {filename}, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly.", LogLevel.Warn); + break; + + case InstructionHandleResult.NotCompatible: + if (!assumeCompatible) + throw new IncompatibleInstructionException(rewriter.NounPhrase, $"Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {filename}."); + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {filename}, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly.", LogLevel.Warn); + break; + + case InstructionHandleResult.None: + break; + + default: + throw new NotSupportedException($"Unrecognised instruction handler result '{result}'."); } } } diff --git a/src/StardewModdingAPI/Framework/ModLoading/Finders/EventFinder.cs b/src/StardewModdingAPI/Framework/ModLoading/Finders/EventFinder.cs index ac5034c4..cd65b2dd 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/Finders/EventFinder.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/Finders/EventFinder.cs @@ -3,8 +3,8 @@ using Mono.Cecil.Cil; namespace StardewModdingAPI.Framework.ModLoading.Finders { - /// Finds incompatible CIL instructions that reference a given event and throws an . - internal class EventFinder : IInstructionRewriter + /// Finds incompatible CIL instructions that reference a given event. + internal class EventFinder : IInstructionHandler { /********* ** Properties @@ -15,6 +15,9 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders /// The event name for which to find references. private readonly string EventName; + /// The result to return for matching instructions. + private readonly InstructionHandleResult Result; + /********* ** Accessors @@ -29,42 +32,38 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders /// Construct an instance. /// The full type name for which to find references. /// The event name for which to find references. - /// A brief noun phrase indicating what the instruction finder matches (or null to generate one). - public EventFinder(string fullTypeName, string eventName, string nounPhrase = null) + /// The result to return for matching instructions. + public EventFinder(string fullTypeName, string eventName, InstructionHandleResult result) { this.FullTypeName = fullTypeName; this.EventName = eventName; - this.NounPhrase = nounPhrase ?? $"{fullTypeName}.{eventName} event"; + this.Result = result; + this.NounPhrase = $"{fullTypeName}.{eventName} event"; } - /// Rewrite a method definition for compatibility. - /// The mod to which the module belongs. - /// The module being rewritten. - /// The method definition to rewrite. + /// Perform the predefined logic for a method if applicable. + /// The mod containing the instruction. + /// The assembly module containing the instruction. + /// The method definition containing the instruction. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - /// Returns whether the instruction was rewritten. - /// The CIL instruction is not compatible, and can't be rewritten. - public virtual bool Rewrite(IModMetadata mod, ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + public virtual InstructionHandleResult Handle(IModMetadata mod, ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) { - return false; + return InstructionHandleResult.None; } - /// Rewrite a CIL instruction for compatibility. - /// The mod to which the module belongs. - /// The module being rewritten. - /// The CIL rewriter. - /// The instruction to rewrite. + /// Perform the predefined logic for an instruction if applicable. + /// The mod containing the instruction. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - /// Returns whether the instruction was rewritten. - /// The CIL instruction is not compatible, and can't be rewritten. - public virtual bool Rewrite(IModMetadata mod, ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + public virtual InstructionHandleResult Handle(IModMetadata mod, ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { - if (!this.IsMatch(instruction)) - return false; - - throw new IncompatibleInstructionException(this.NounPhrase); + return this.IsMatch(instruction) + ? this.Result + : InstructionHandleResult.None; } diff --git a/src/StardewModdingAPI/Framework/ModLoading/Finders/FieldFinder.cs b/src/StardewModdingAPI/Framework/ModLoading/Finders/FieldFinder.cs index 008399d5..1ec4483e 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/Finders/FieldFinder.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/Finders/FieldFinder.cs @@ -3,8 +3,8 @@ using Mono.Cecil.Cil; namespace StardewModdingAPI.Framework.ModLoading.Finders { - /// Finds incompatible CIL instructions that reference a given field and throws an . - internal class FieldFinder : IInstructionRewriter + /// Finds incompatible CIL instructions that reference a given field. + internal class FieldFinder : IInstructionHandler { /********* ** Properties @@ -15,6 +15,9 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders /// The field name for which to find references. private readonly string FieldName; + /// The result to return for matching instructions. + private readonly InstructionHandleResult Result; + /********* ** Accessors @@ -29,42 +32,38 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders /// Construct an instance. /// The full type name for which to find references. /// The field name for which to find references. - /// A brief noun phrase indicating what the instruction finder matches (or null to generate one). - public FieldFinder(string fullTypeName, string fieldName, string nounPhrase = null) + /// The result to return for matching instructions. + public FieldFinder(string fullTypeName, string fieldName, InstructionHandleResult result) { this.FullTypeName = fullTypeName; this.FieldName = fieldName; - this.NounPhrase = nounPhrase ?? $"{fullTypeName}.{fieldName} field"; + this.Result = result; + this.NounPhrase = $"{fullTypeName}.{fieldName} field"; } - /// Rewrite a method definition for compatibility. - /// The mod to which the module belongs. - /// The module being rewritten. - /// The method definition to rewrite. + /// Perform the predefined logic for a method if applicable. + /// The mod containing the instruction. + /// The assembly module containing the instruction. + /// The method definition containing the instruction. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - /// Returns whether the instruction was rewritten. - /// The CIL instruction is not compatible, and can't be rewritten. - public virtual bool Rewrite(IModMetadata mod, ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + public virtual InstructionHandleResult Handle(IModMetadata mod, ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) { - return false; + return InstructionHandleResult.None; } - /// Rewrite a CIL instruction for compatibility. - /// The mod to which the module belongs. - /// The module being rewritten. - /// The CIL rewriter. - /// The instruction to rewrite. + /// Perform the predefined logic for an instruction if applicable. + /// The mod containing the instruction. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - /// Returns whether the instruction was rewritten. - /// The CIL instruction is not compatible, and can't be rewritten. - public virtual bool Rewrite(IModMetadata mod, ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + public virtual InstructionHandleResult Handle(IModMetadata mod, ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { - if (!this.IsMatch(instruction)) - return false; - - throw new IncompatibleInstructionException(this.NounPhrase); + return this.IsMatch(instruction) + ? this.Result + : InstructionHandleResult.None; } diff --git a/src/StardewModdingAPI/Framework/ModLoading/Finders/MethodFinder.cs b/src/StardewModdingAPI/Framework/ModLoading/Finders/MethodFinder.cs index 2a6dc99e..4924c6e2 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/Finders/MethodFinder.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/Finders/MethodFinder.cs @@ -3,8 +3,8 @@ using Mono.Cecil.Cil; namespace StardewModdingAPI.Framework.ModLoading.Finders { - /// Finds incompatible CIL instructions that reference a given method and throws an . - internal class MethodFinder : IInstructionRewriter + /// Finds incompatible CIL instructions that reference a given method. + internal class MethodFinder : IInstructionHandler { /********* ** Properties @@ -15,6 +15,9 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders /// The method name for which to find references. private readonly string MethodName; + /// The result to return for matching instructions. + private readonly InstructionHandleResult Result; + /********* ** Accessors @@ -29,42 +32,38 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders /// Construct an instance. /// The full type name for which to find references. /// The method name for which to find references. - /// A brief noun phrase indicating what the instruction finder matches (or null to generate one). - public MethodFinder(string fullTypeName, string methodName, string nounPhrase = null) + /// The result to return for matching instructions. + public MethodFinder(string fullTypeName, string methodName, InstructionHandleResult result) { this.FullTypeName = fullTypeName; this.MethodName = methodName; - this.NounPhrase = nounPhrase ?? $"{fullTypeName}.{methodName} method"; + this.Result = result; + this.NounPhrase = $"{fullTypeName}.{methodName} method"; } - /// Rewrite a method definition for compatibility. - /// The mod to which the module belongs. - /// The module being rewritten. - /// The method definition to rewrite. + /// Perform the predefined logic for a method if applicable. + /// The mod containing the instruction. + /// The assembly module containing the instruction. + /// The method definition containing the instruction. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - /// Returns whether the instruction was rewritten. - /// The CIL instruction is not compatible, and can't be rewritten. - public virtual bool Rewrite(IModMetadata mod, ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + public virtual InstructionHandleResult Handle(IModMetadata mod, ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) { - return false; + return InstructionHandleResult.None; } - /// Rewrite a CIL instruction for compatibility. - /// The mod to which the module belongs. - /// The module being rewritten. - /// The CIL rewriter. - /// The instruction to rewrite. + /// Perform the predefined logic for an instruction if applicable. + /// The mod containing the instruction. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - /// Returns whether the instruction was rewritten. - /// The CIL instruction is not compatible, and can't be rewritten. - public bool Rewrite(IModMetadata mod, ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + public InstructionHandleResult Handle(IModMetadata mod, ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { - if (!this.IsMatch(instruction)) - return false; - - throw new IncompatibleInstructionException(this.NounPhrase); + return this.IsMatch(instruction) + ? this.Result + : InstructionHandleResult.None; } diff --git a/src/StardewModdingAPI/Framework/ModLoading/Finders/PropertyFinder.cs b/src/StardewModdingAPI/Framework/ModLoading/Finders/PropertyFinder.cs index a0ce1cbf..37392f77 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/Finders/PropertyFinder.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/Finders/PropertyFinder.cs @@ -3,8 +3,8 @@ using Mono.Cecil.Cil; namespace StardewModdingAPI.Framework.ModLoading.Finders { - /// Finds incompatible CIL instructions that reference a given property and throws an . - internal class PropertyFinder : IInstructionRewriter + /// Finds incompatible CIL instructions that reference a given property. + internal class PropertyFinder : IInstructionHandler { /********* ** Properties @@ -15,6 +15,9 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders /// The property name for which to find references. private readonly string PropertyName; + /// The result to return for matching instructions. + private readonly InstructionHandleResult Result; + /********* ** Accessors @@ -29,42 +32,38 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders /// Construct an instance. /// The full type name for which to find references. /// The property name for which to find references. - /// A brief noun phrase indicating what the instruction finder matches (or null to generate one). - public PropertyFinder(string fullTypeName, string propertyName, string nounPhrase = null) + /// The result to return for matching instructions. + public PropertyFinder(string fullTypeName, string propertyName, InstructionHandleResult result) { this.FullTypeName = fullTypeName; this.PropertyName = propertyName; - this.NounPhrase = nounPhrase ?? $"{fullTypeName}.{propertyName} property"; + this.Result = result; + this.NounPhrase = $"{fullTypeName}.{propertyName} property"; } - /// Rewrite a method definition for compatibility. - /// The mod to which the module belongs. - /// The module being rewritten. - /// The method definition to rewrite. + /// Perform the predefined logic for a method if applicable. + /// The mod containing the instruction. + /// The assembly module containing the instruction. + /// The method definition containing the instruction. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - /// Returns whether the instruction was rewritten. - /// The CIL instruction is not compatible, and can't be rewritten. - public virtual bool Rewrite(IModMetadata mod, ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + public virtual InstructionHandleResult Handle(IModMetadata mod, ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) { - return false; + return InstructionHandleResult.None; } - /// Rewrite a CIL instruction for compatibility. - /// The mod to which the module belongs. - /// The module being rewritten. - /// The CIL rewriter. - /// The instruction to rewrite. + /// Perform the predefined logic for an instruction if applicable. + /// The mod containing the instruction. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - /// Returns whether the instruction was rewritten. - /// The CIL instruction is not compatible, and can't be rewritten. - public virtual bool Rewrite(IModMetadata mod, ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + public virtual InstructionHandleResult Handle(IModMetadata mod, ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { - if (!this.IsMatch(instruction)) - return false; - - throw new IncompatibleInstructionException(this.NounPhrase); + return this.IsMatch(instruction) + ? this.Result + : InstructionHandleResult.None; } diff --git a/src/StardewModdingAPI/Framework/ModLoading/Finders/TypeFinder.cs b/src/StardewModdingAPI/Framework/ModLoading/Finders/TypeFinder.cs index a3005c85..a0703669 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/Finders/TypeFinder.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/Finders/TypeFinder.cs @@ -4,8 +4,8 @@ using Mono.Cecil.Cil; namespace StardewModdingAPI.Framework.ModLoading.Finders { - /// Finds incompatible CIL instructions that reference a given type and throws an . - internal class TypeFinder : IInstructionRewriter + /// Finds incompatible CIL instructions that reference a given type. + internal class TypeFinder : IInstructionHandler { /********* ** Accessors @@ -13,6 +13,9 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders /// The full type name for which to find references. private readonly string FullTypeName; + /// The result to return for matching instructions. + private readonly InstructionHandleResult Result; + /********* ** Accessors @@ -26,44 +29,39 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders *********/ /// Construct an instance. /// The full type name to match. - /// A brief noun phrase indicating what the instruction finder matches (or null to generate one). - public TypeFinder(string fullTypeName, string nounPhrase = null) + /// The result to return for matching instructions. + public TypeFinder(string fullTypeName, InstructionHandleResult result) { this.FullTypeName = fullTypeName; - this.NounPhrase = nounPhrase ?? $"{fullTypeName} type"; + this.Result = result; + this.NounPhrase = $"{fullTypeName} type"; } - /// Rewrite a method definition for compatibility. - /// The mod to which the module belongs. - /// The module being rewritten. - /// The method definition to rewrite. + /// Perform the predefined logic for a method if applicable. + /// The mod containing the instruction. + /// The assembly module containing the instruction. + /// The method definition containing the instruction. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - /// Returns whether the instruction was rewritten. - /// The CIL instruction is not compatible, and can't be rewritten. - public virtual bool Rewrite(IModMetadata mod, ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + public virtual InstructionHandleResult Handle(IModMetadata mod, ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) { - if (!this.IsMatch(method)) - return false; - - throw new IncompatibleInstructionException(this.NounPhrase); + return this.IsMatch(method) + ? this.Result + : InstructionHandleResult.None; } - /// Rewrite a CIL instruction for compatibility. - /// The mod to which the module belongs. - /// The module being rewritten. - /// The CIL rewriter. - /// The instruction to rewrite. + /// Perform the predefined logic for an instruction if applicable. + /// The mod containing the instruction. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - /// Returns whether the instruction was rewritten. - /// The CIL instruction is not compatible, and can't be rewritten. - public virtual bool Rewrite(IModMetadata mod, ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + public virtual InstructionHandleResult Handle(IModMetadata mod, ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { - if (!this.IsMatch(instruction)) - return false; - - throw new IncompatibleInstructionException(this.NounPhrase); + return this.IsMatch(instruction) + ? this.Result + : InstructionHandleResult.None; } diff --git a/src/StardewModdingAPI/Framework/ModLoading/IInstructionRewriter.cs b/src/StardewModdingAPI/Framework/ModLoading/IInstructionRewriter.cs index ce12c717..be2e10ee 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/IInstructionRewriter.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/IInstructionRewriter.cs @@ -3,38 +3,34 @@ using Mono.Cecil.Cil; namespace StardewModdingAPI.Framework.ModLoading { - /// Rewrites CIL instructions for compatibility. - internal interface IInstructionRewriter + /// Performs predefined logic for detected CIL instructions. + internal interface IInstructionHandler { /********* ** Accessors *********/ - /// A brief noun phrase indicating what the rewriter matches. + /// A brief noun phrase indicating what the handler matches. string NounPhrase { get; } /********* ** Methods *********/ - /// Rewrite a method definition for compatibility. - /// The mod to which the module belongs. - /// The module being rewritten. - /// The method definition to rewrite. + /// Perform the predefined logic for a method if applicable. + /// The mod containing the instruction. + /// The assembly module containing the instruction. + /// The method definition containing the instruction. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - /// Returns whether the instruction was rewritten. - /// The CIL instruction is not compatible, and can't be rewritten. - bool Rewrite(IModMetadata mod, ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged); + InstructionHandleResult Handle(IModMetadata mod, ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged); - /// Rewrite a CIL instruction for compatibility. - /// The mod to which the module belongs. - /// The module being rewritten. - /// The CIL rewriter. - /// The instruction to rewrite. + /// Perform the predefined logic for an instruction if applicable. + /// The mod containing the instruction. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - /// Returns whether the instruction was rewritten. - /// The CIL instruction is not compatible, and can't be rewritten. - bool Rewrite(IModMetadata mod, ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged); + InstructionHandleResult Handle(IModMetadata mod, ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged); } } diff --git a/src/StardewModdingAPI/Framework/ModLoading/InstructionHandleResult.cs b/src/StardewModdingAPI/Framework/ModLoading/InstructionHandleResult.cs new file mode 100644 index 00000000..3921e9c4 --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModLoading/InstructionHandleResult.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Framework.ModLoading +{ + /// Indicates how an instruction was handled. + internal enum InstructionHandleResult + { + /// No special handling is needed. + None, + + /// The instruction was successfully rewritten for compatibility. + Rewritten, + + /// The instruction is not compatible and can't be rewritten for compatibility. + NotCompatible + } +} diff --git a/src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs index fb2a9a96..324d1d1e 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs @@ -23,32 +23,29 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters /// The type whose field to which references should be rewritten. /// The field name to rewrite. /// The new field name to reference. - /// A brief noun phrase indicating what the instruction finder matches (or null to generate one). - public FieldReplaceRewriter(Type type, string fromFieldName, string toFieldName, string nounPhrase = null) - : base(type.FullName, fromFieldName, nounPhrase) + public FieldReplaceRewriter(Type type, string fromFieldName, string toFieldName) + : base(type.FullName, fromFieldName, InstructionHandleResult.None) { this.ToField = type.GetField(toFieldName); if (this.ToField == null) throw new InvalidOperationException($"The {type.FullName} class doesn't have a {toFieldName} field."); } - /// Rewrite a CIL instruction for compatibility. - /// The mod to which the module belongs. - /// The module being rewritten. - /// The CIL rewriter. - /// The instruction to rewrite. + /// Perform the predefined logic for an instruction if applicable. + /// The mod containing the instruction. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - /// Returns whether the instruction was rewritten. - /// The CIL instruction is not compatible, and can't be rewritten. - public override bool Rewrite(IModMetadata mod, ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + public override InstructionHandleResult Handle(IModMetadata mod, ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { if (!this.IsMatch(instruction)) - return false; + return InstructionHandleResult.None; FieldReference newRef = module.Import(this.ToField); cil.Replace(instruction, cil.Create(instruction.OpCode, newRef)); - return true; + return InstructionHandleResult.Rewritten; } } } diff --git a/src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs index 03d1f707..1b005869 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs @@ -24,32 +24,29 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters /// Construct an instance. /// The type whose field to which references should be rewritten. /// The field name to rewrite. - /// A brief noun phrase indicating what the instruction finder matches (or null to generate one). - public FieldToPropertyRewriter(Type type, string fieldName, string nounPhrase = null) - : base(type.FullName, fieldName, nounPhrase) + public FieldToPropertyRewriter(Type type, string fieldName) + : base(type.FullName, fieldName, InstructionHandleResult.None) { this.Type = type; this.FieldName = fieldName; } - /// Rewrite a CIL instruction for compatibility. - /// The mod to which the module belongs. - /// The module being rewritten. - /// The CIL rewriter. - /// The instruction to rewrite. + /// Perform the predefined logic for an instruction if applicable. + /// The mod containing the instruction. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - /// Returns whether the instruction was rewritten. - /// The CIL instruction is not compatible, and can't be rewritten. - public override bool Rewrite(IModMetadata mod, ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + public override InstructionHandleResult Handle(IModMetadata mod, ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { if (!this.IsMatch(instruction)) - return false; + return InstructionHandleResult.None; string methodPrefix = instruction.OpCode == OpCodes.Ldsfld || instruction.OpCode == OpCodes.Ldfld ? "get" : "set"; MethodReference propertyRef = module.Import(this.Type.GetMethod($"{methodPrefix}_{this.FieldName}")); cil.Replace(instruction, cil.Create(OpCodes.Call, propertyRef)); - return true; + return InstructionHandleResult.Rewritten; } } } diff --git a/src/StardewModdingAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs index 1e116e1f..50338071 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs @@ -5,7 +5,7 @@ using Mono.Cecil.Cil; namespace StardewModdingAPI.Framework.ModLoading.Rewriters { /// Rewrites method references from one parent type to another if the signatures match. - internal class MethodParentRewriter : IInstructionRewriter + internal class MethodParentRewriter : IInstructionHandler { /********* ** Properties @@ -34,45 +34,40 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters /// The type whose methods to remap. /// The type with methods to map to. /// Whether to only rewrite references if loading the assembly on a different platform than it was compiled on. - /// A brief noun phrase indicating what the instruction finder matches (or null to generate one). - public MethodParentRewriter(Type fromType, Type toType, bool onlyIfPlatformChanged = false, string nounPhrase = null) + public MethodParentRewriter(Type fromType, Type toType, bool onlyIfPlatformChanged = false) { this.FromType = fromType; this.ToType = toType; - this.NounPhrase = nounPhrase ?? $"{fromType.Name} methods"; + this.NounPhrase = $"{fromType.Name} methods"; this.OnlyIfPlatformChanged = onlyIfPlatformChanged; } - /// Rewrite a method definition for compatibility. - /// The mod to which the module belongs. - /// The module being rewritten. - /// The method definition to rewrite. + /// Perform the predefined logic for a method if applicable. + /// The mod containing the instruction. + /// The assembly module containing the instruction. + /// The method definition containing the instruction. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - /// Returns whether the instruction was rewritten. - /// The CIL instruction is not compatible, and can't be rewritten. - public bool Rewrite(IModMetadata mod, ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + public InstructionHandleResult Handle(IModMetadata mod, ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) { - return false; + return InstructionHandleResult.None; } - /// Rewrite a CIL instruction for compatibility. - /// The mod to which the module belongs. - /// The module being rewritten. - /// The CIL rewriter. - /// The instruction to rewrite. + /// Perform the predefined logic for an instruction if applicable. + /// The mod containing the instruction. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - /// Returns whether the instruction was rewritten. - /// The CIL instruction is not compatible, and can't be rewritten. - public bool Rewrite(IModMetadata mod, ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + public InstructionHandleResult Handle(IModMetadata mod, ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { if (!this.IsMatch(instruction, platformChanged)) - return false; + return InstructionHandleResult.None; MethodReference methodRef = (MethodReference)instruction.Operand; methodRef.DeclaringType = module.Import(this.ToType); - return true; + return InstructionHandleResult.Rewritten; } diff --git a/src/StardewModdingAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs index 8db39cfe..df6e5e4b 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs @@ -24,23 +24,20 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters /// Construct an instance. /// The full type name to which to find references. /// The new type to reference. - /// A brief noun phrase indicating what the instruction finder matches (or null to generate one). - public TypeReferenceRewriter(string fromTypeFullName, Type toType, string nounPhrase = null) - : base(fromTypeFullName, nounPhrase) + public TypeReferenceRewriter(string fromTypeFullName, Type toType) + : base(fromTypeFullName, InstructionHandleResult.None) { this.FromTypeName = fromTypeFullName; this.ToType = toType; } - /// Rewrite a method definition for compatibility. - /// The mod to which the module belongs. - /// The module being rewritten. - /// The method definition to rewrite. + /// Perform the predefined logic for a method if applicable. + /// The mod containing the instruction. + /// The assembly module containing the instruction. + /// The method definition containing the instruction. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - /// Returns whether the instruction was rewritten. - /// The CIL instruction is not compatible, and can't be rewritten. - public override bool Rewrite(IModMetadata mod, ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + public override InstructionHandleResult Handle(IModMetadata mod, ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) { bool rewritten = false; @@ -84,22 +81,22 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters } } - return rewritten; + return rewritten + ? InstructionHandleResult.Rewritten + : InstructionHandleResult.None; } - /// Rewrite a CIL instruction for compatibility. - /// The mod to which the module belongs. - /// The module being rewritten. - /// The CIL rewriter. - /// The instruction to rewrite. + /// Perform the predefined logic for an instruction if applicable. + /// The mod containing the instruction. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - /// Returns whether the instruction was rewritten. - /// The CIL instruction is not compatible, and can't be rewritten. - public override bool Rewrite(IModMetadata mod, ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + public override InstructionHandleResult Handle(IModMetadata mod, ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { if (!this.IsMatch(instruction) && !instruction.ToString().Contains(this.FromTypeName)) - return false; + return InstructionHandleResult.None; // field reference FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); @@ -127,14 +124,14 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters cil.Replace(instruction, cil.Create(instruction.OpCode, newRef)); } - return true; + return InstructionHandleResult.Rewritten; } /********* ** Private methods *********/ /// Get the adjusted type reference if it matches, else the same value. - /// The module being rewritten. + /// The assembly module containing the instruction. /// The type to replace if it matches. private TypeReference RewriteIfNeeded(ModuleDefinition module, TypeReference type) { diff --git a/src/StardewModdingAPI/Metadata/InstructionMetadata.cs b/src/StardewModdingAPI/Metadata/InstructionMetadata.cs index 67fdc041..fc4f112c 100644 --- a/src/StardewModdingAPI/Metadata/InstructionMetadata.cs +++ b/src/StardewModdingAPI/Metadata/InstructionMetadata.cs @@ -16,61 +16,61 @@ namespace StardewModdingAPI.Metadata ** Public methods *********/ /// Get rewriters which detect or fix incompatible CIL instructions in mod assemblies. - public IEnumerable GetRewriters() + public IEnumerable GetHandlers() { - return new IInstructionRewriter[] + return new IInstructionHandler[] { /**** - ** Finders throw an exception when incompatible code is found. + ** throw exception for incompatible code ****/ // changes in Stardew Valley 1.2 (with no rewriters) - new FieldFinder("StardewValley.Item", "set_Name"), + new FieldFinder("StardewValley.Item", "set_Name", InstructionHandleResult.NotCompatible), // APIs removed in SMAPI 1.9 - new TypeFinder("StardewModdingAPI.Advanced.ConfigFile"), - new TypeFinder("StardewModdingAPI.Advanced.IConfigFile"), - new TypeFinder("StardewModdingAPI.Entities.SPlayer"), - new TypeFinder("StardewModdingAPI.Extensions"), - new TypeFinder("StardewModdingAPI.Inheritance.SGame"), - new TypeFinder("StardewModdingAPI.Inheritance.SObject"), - new TypeFinder("StardewModdingAPI.LogWriter"), - new TypeFinder("StardewModdingAPI.Manifest"), - new TypeFinder("StardewModdingAPI.Version"), - new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "DrawDebug"), - new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "DrawTick"), - new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPostRenderHudEventNoCheck"), - new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPostRenderGuiEventNoCheck"), - new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPreRenderHudEventNoCheck"), - new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPreRenderGuiEventNoCheck"), + new TypeFinder("StardewModdingAPI.Advanced.ConfigFile", InstructionHandleResult.NotCompatible), + new TypeFinder("StardewModdingAPI.Advanced.IConfigFile", InstructionHandleResult.NotCompatible), + new TypeFinder("StardewModdingAPI.Entities.SPlayer", InstructionHandleResult.NotCompatible), + new TypeFinder("StardewModdingAPI.Extensions", InstructionHandleResult.NotCompatible), + new TypeFinder("StardewModdingAPI.Inheritance.SGame", InstructionHandleResult.NotCompatible), + new TypeFinder("StardewModdingAPI.Inheritance.SObject", InstructionHandleResult.NotCompatible), + new TypeFinder("StardewModdingAPI.LogWriter", InstructionHandleResult.NotCompatible), + new TypeFinder("StardewModdingAPI.Manifest", InstructionHandleResult.NotCompatible), + new TypeFinder("StardewModdingAPI.Version", InstructionHandleResult.NotCompatible), + new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "DrawDebug", InstructionHandleResult.NotCompatible), + new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "DrawTick", InstructionHandleResult.NotCompatible), + new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPostRenderHudEventNoCheck", InstructionHandleResult.NotCompatible), + new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPostRenderGuiEventNoCheck", InstructionHandleResult.NotCompatible), + new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPreRenderHudEventNoCheck", InstructionHandleResult.NotCompatible), + new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPreRenderGuiEventNoCheck", InstructionHandleResult.NotCompatible), // APIs removed in SMAPI 2.0 #if !SMAPI_1_x - new TypeFinder("StardewModdingAPI.Command"), - new TypeFinder("StardewModdingAPI.Config"), - new TypeFinder("StardewModdingAPI.Log"), - new EventFinder("StardewModdingAPI.Events.GameEvents", "Initialize"), - new EventFinder("StardewModdingAPI.Events.GameEvents", "LoadContent"), - new EventFinder("StardewModdingAPI.Events.GameEvents", "GameLoaded"), - new EventFinder("StardewModdingAPI.Events.GameEvents", "FirstUpdateTick"), - new EventFinder("StardewModdingAPI.Events.PlayerEvents", "LoadedGame"), - new EventFinder("StardewModdingAPI.Events.PlayerEvents", "FarmerChanged"), - new EventFinder("StardewModdingAPI.Events.TimeEvents", "DayOfMonthChanged"), - new EventFinder("StardewModdingAPI.Events.TimeEvents", "YearOfGameChanged"), - new EventFinder("StardewModdingAPI.Events.TimeEvents", "SeasonOfYearChanged"), - new EventFinder("StardewModdingAPI.Events.TimeEvents", "OnNewDay"), - new TypeFinder("StardewModdingAPI.Events.EventArgsCommand"), - new TypeFinder("StardewModdingAPI.Events.EventArgsFarmerChanged"), - new TypeFinder("StardewModdingAPI.Events.EventArgsLoadedGameChanged"), - new TypeFinder("StardewModdingAPI.Events.EventArgsNewDay"), - new TypeFinder("StardewModdingAPI.Events.EventArgsStringChanged"), - new PropertyFinder("StardewModdingAPI.Mod", "PathOnDisk"), - new PropertyFinder("StardewModdingAPI.Mod", "BaseConfigPath"), - new PropertyFinder("StardewModdingAPI.Mod", "PerSaveConfigFolder"), - new PropertyFinder("StardewModdingAPI.Mod", "PerSaveConfigPath"), + new TypeFinder("StardewModdingAPI.Command", InstructionHandleResult.NotCompatible), + new TypeFinder("StardewModdingAPI.Config", InstructionHandleResult.NotCompatible), + new TypeFinder("StardewModdingAPI.Log", InstructionHandleResult.NotCompatible), + new EventFinder("StardewModdingAPI.Events.GameEvents", "Initialize", InstructionHandleResult.NotCompatible), + new EventFinder("StardewModdingAPI.Events.GameEvents", "LoadContent", InstructionHandleResult.NotCompatible), + new EventFinder("StardewModdingAPI.Events.GameEvents", "GameLoaded", InstructionHandleResult.NotCompatible), + new EventFinder("StardewModdingAPI.Events.GameEvents", "FirstUpdateTick", InstructionHandleResult.NotCompatible), + new EventFinder("StardewModdingAPI.Events.PlayerEvents", "LoadedGame", InstructionHandleResult.NotCompatible), + new EventFinder("StardewModdingAPI.Events.PlayerEvents", "FarmerChanged", InstructionHandleResult.NotCompatible), + new EventFinder("StardewModdingAPI.Events.TimeEvents", "DayOfMonthChanged", InstructionHandleResult.NotCompatible), + new EventFinder("StardewModdingAPI.Events.TimeEvents", "YearOfGameChanged", InstructionHandleResult.NotCompatible), + new EventFinder("StardewModdingAPI.Events.TimeEvents", "SeasonOfYearChanged", InstructionHandleResult.NotCompatible), + new EventFinder("StardewModdingAPI.Events.TimeEvents", "OnNewDay", InstructionHandleResult.NotCompatible), + new TypeFinder("StardewModdingAPI.Events.EventArgsCommand", InstructionHandleResult.NotCompatible), + new TypeFinder("StardewModdingAPI.Events.EventArgsFarmerChanged", InstructionHandleResult.NotCompatible), + new TypeFinder("StardewModdingAPI.Events.EventArgsLoadedGameChanged", InstructionHandleResult.NotCompatible), + new TypeFinder("StardewModdingAPI.Events.EventArgsNewDay", InstructionHandleResult.NotCompatible), + new TypeFinder("StardewModdingAPI.Events.EventArgsStringChanged", InstructionHandleResult.NotCompatible), + new PropertyFinder("StardewModdingAPI.Mod", "PathOnDisk", InstructionHandleResult.NotCompatible), + new PropertyFinder("StardewModdingAPI.Mod", "BaseConfigPath", InstructionHandleResult.NotCompatible), + new PropertyFinder("StardewModdingAPI.Mod", "PerSaveConfigFolder", InstructionHandleResult.NotCompatible), + new PropertyFinder("StardewModdingAPI.Mod", "PerSaveConfigPath", InstructionHandleResult.NotCompatible), #endif /**** - ** Rewriters change CIL as needed to fix incompatible code + ** rewrite CIL to fix incompatible code ****/ // crossplatform new MethodParentRewriter(typeof(SpriteBatch), typeof(SpriteBatchWrapper), onlyIfPlatformChanged: true), diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index d4b1cb69..8b3b6936 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -98,6 +98,7 @@ + From 1cf7c2e87264c8181b8663781393e9a0310f78a0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 19 Sep 2017 23:43:41 -0400 Subject: [PATCH 055/186] rename file to match new type name (#347) --- .../{IInstructionRewriter.cs => IInstructionHandler.cs} | 0 src/StardewModdingAPI/StardewModdingAPI.csproj | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/StardewModdingAPI/Framework/ModLoading/{IInstructionRewriter.cs => IInstructionHandler.cs} (100%) diff --git a/src/StardewModdingAPI/Framework/ModLoading/IInstructionRewriter.cs b/src/StardewModdingAPI/Framework/ModLoading/IInstructionHandler.cs similarity index 100% rename from src/StardewModdingAPI/Framework/ModLoading/IInstructionRewriter.cs rename to src/StardewModdingAPI/Framework/ModLoading/IInstructionHandler.cs diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 8b3b6936..175fc3f3 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -96,7 +96,7 @@ - + From ab135fbd84483da661889947bd65cbd5eb72f74e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 19 Sep 2017 23:53:22 -0400 Subject: [PATCH 056/186] encapsulated duplicate code (#347) --- .../Framework/ModLoading/AssemblyLoader.cs | 77 +++++++++---------- 1 file changed, 37 insertions(+), 40 deletions(-) diff --git a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs index 3a8e4fa9..b78bf6bb 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs @@ -221,53 +221,21 @@ namespace StardewModdingAPI.Framework.ModLoading foreach (IInstructionHandler handler in handlers) { InstructionHandleResult result = handler.Handle(mod, module, method, this.AssemblyMap, platformChanged); - switch (result) - { - case InstructionHandleResult.Rewritten: - this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Rewrote {filename} to fix {handler.NounPhrase}..."); - anyRewritten = true; - break; - - case InstructionHandleResult.NotCompatible: - if (!assumeCompatible) - throw new IncompatibleInstructionException(handler.NounPhrase, $"Found an incompatible CIL instruction ({handler.NounPhrase}) while loading assembly {filename}."); - this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Found an incompatible CIL instruction ({handler.NounPhrase}) while loading assembly {filename}, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly.", LogLevel.Warn); - break; - - case InstructionHandleResult.None: - break; - - default: - throw new NotSupportedException($"Unrecognised instruction handler result '{result}'."); - } + this.ProcessInstructionHandleResult(handler, result, loggedMessages, logPrefix, assumeCompatible, filename); + if (result == InstructionHandleResult.Rewritten) + anyRewritten = true; } // check CIL instructions ILProcessor cil = method.Body.GetILProcessor(); foreach (Instruction instruction in cil.Body.Instructions.ToArray()) { - foreach (IInstructionHandler rewriter in handlers) + foreach (IInstructionHandler handler in handlers) { - InstructionHandleResult result = rewriter.Handle(mod, module, cil, instruction, this.AssemblyMap, platformChanged); - switch (result) - { - case InstructionHandleResult.Rewritten: - this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Rewrote {filename} to fix {rewriter.NounPhrase}..."); - anyRewritten = true; - break; - - case InstructionHandleResult.NotCompatible: - if (!assumeCompatible) - throw new IncompatibleInstructionException(rewriter.NounPhrase, $"Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {filename}."); - this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {filename}, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly.", LogLevel.Warn); - break; - - case InstructionHandleResult.None: - break; - - default: - throw new NotSupportedException($"Unrecognised instruction handler result '{result}'."); - } + InstructionHandleResult result = handler.Handle(mod, module, cil, instruction, this.AssemblyMap, platformChanged); + this.ProcessInstructionHandleResult(handler, result, loggedMessages, logPrefix, assumeCompatible, filename); + if (result == InstructionHandleResult.Rewritten) + anyRewritten = true; } } } @@ -275,6 +243,35 @@ namespace StardewModdingAPI.Framework.ModLoading return platformChanged || anyRewritten; } + /// Process the result from an instruction handler. + /// The instruction handler. + /// The result returned by the handler. + /// The messages already logged for the current mod. + /// Assume the mod is compatible, even if incompatible code is detected. + /// A string to prefix to log messages. + /// The assembly filename for log messages. + private void ProcessInstructionHandleResult(IInstructionHandler handler, InstructionHandleResult result, HashSet loggedMessages, string logPrefix, bool assumeCompatible, string filename) + { + switch (result) + { + case InstructionHandleResult.Rewritten: + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Rewrote {filename} to fix {handler.NounPhrase}..."); + break; + + case InstructionHandleResult.NotCompatible: + if (!assumeCompatible) + throw new IncompatibleInstructionException(handler.NounPhrase, $"Found an incompatible CIL instruction ({handler.NounPhrase}) while loading assembly {filename}."); + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Found an incompatible CIL instruction ({handler.NounPhrase}) while loading assembly {filename}, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly.", LogLevel.Warn); + break; + + case InstructionHandleResult.None: + break; + + default: + throw new NotSupportedException($"Unrecognised instruction handler result '{result}'."); + } + } + /// Get the correct reference to use for compatibility with the current platform. /// The type reference to rewrite. private void ChangeTypeScope(TypeReference type) From 1aa44b2624936a3543dd82329e29732b5278affc Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 20 Sep 2017 00:08:04 -0400 Subject: [PATCH 057/186] add support for detecting game patching via Harmony (#347) --- release-notes.md | 5 +++-- .../Framework/ModLoading/AssemblyLoader.cs | 19 +++++++++++++------ .../ModLoading/InstructionHandleResult.cs | 5 ++++- .../Metadata/InstructionMetadata.cs | 5 +++++ .../StardewModdingAPI.csproj | 1 - 5 files changed, 25 insertions(+), 10 deletions(-) diff --git a/release-notes.md b/release-notes.md index 5e01a264..6d430c7a 100644 --- a/release-notes.md +++ b/release-notes.md @@ -3,8 +3,9 @@ For players: -* The SMAPI console is now much simpler and easier to read. -* The SMAPI console now adjusts its colors when you have a light terminal background. +* The console is now simpler and easier to read. +* The console now adjusts its colors when you have a light terminal background. +* SMAPI now detects mods which may impact game stability and shows a warning in the console. * Updated compatibility list. For mod developers: diff --git a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs index b78bf6bb..835ef631 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs @@ -83,12 +83,13 @@ namespace StardewModdingAPI.Framework.ModLoading // rewrite & load assemblies in leaf-to-root order bool oneAssembly = assemblies.Length == 1; Assembly lastAssembly = null; + HashSet loggedMessages = new HashSet(); foreach (AssemblyParseResult assembly in assemblies) { if (assembly.Status == AssemblyLoadStatus.AlreadyLoaded) continue; - bool changed = this.RewriteAssembly(mod, assembly.Definition, assumeCompatible, logPrefix: " "); + bool changed = this.RewriteAssembly(mod, assembly.Definition, assumeCompatible, loggedMessages, logPrefix: " "); if (changed) { if (!oneAssembly) @@ -178,13 +179,13 @@ namespace StardewModdingAPI.Framework.ModLoading /// The mod for which the assembly is being loaded. /// The assembly to rewrite. /// Assume the mod is compatible, even if incompatible code is detected. + /// The messages that have already been logged for this mod. /// A string to prefix to log messages. /// Returns whether the assembly was modified. /// An incompatible CIL instruction was found while rewriting the assembly. - private bool RewriteAssembly(IModMetadata mod, AssemblyDefinition assembly, bool assumeCompatible, string logPrefix) + private bool RewriteAssembly(IModMetadata mod, AssemblyDefinition assembly, bool assumeCompatible, HashSet loggedMessages, string logPrefix) { ModuleDefinition module = assembly.MainModule; - HashSet loggedMessages = new HashSet(); string filename = $"{assembly.Name.Name}.dll"; // swap assembly references if needed (e.g. XNA => MonoGame) @@ -221,7 +222,7 @@ namespace StardewModdingAPI.Framework.ModLoading foreach (IInstructionHandler handler in handlers) { InstructionHandleResult result = handler.Handle(mod, module, method, this.AssemblyMap, platformChanged); - this.ProcessInstructionHandleResult(handler, result, loggedMessages, logPrefix, assumeCompatible, filename); + this.ProcessInstructionHandleResult(mod, handler, result, loggedMessages, logPrefix, assumeCompatible, filename); if (result == InstructionHandleResult.Rewritten) anyRewritten = true; } @@ -233,7 +234,7 @@ namespace StardewModdingAPI.Framework.ModLoading foreach (IInstructionHandler handler in handlers) { InstructionHandleResult result = handler.Handle(mod, module, cil, instruction, this.AssemblyMap, platformChanged); - this.ProcessInstructionHandleResult(handler, result, loggedMessages, logPrefix, assumeCompatible, filename); + this.ProcessInstructionHandleResult(mod, handler, result, loggedMessages, logPrefix, assumeCompatible, filename); if (result == InstructionHandleResult.Rewritten) anyRewritten = true; } @@ -244,13 +245,14 @@ namespace StardewModdingAPI.Framework.ModLoading } /// Process the result from an instruction handler. + /// The mod being analysed. /// The instruction handler. /// The result returned by the handler. /// The messages already logged for the current mod. /// Assume the mod is compatible, even if incompatible code is detected. /// A string to prefix to log messages. /// The assembly filename for log messages. - private void ProcessInstructionHandleResult(IInstructionHandler handler, InstructionHandleResult result, HashSet loggedMessages, string logPrefix, bool assumeCompatible, string filename) + private void ProcessInstructionHandleResult(IModMetadata mod, IInstructionHandler handler, InstructionHandleResult result, HashSet loggedMessages, string logPrefix, bool assumeCompatible, string filename) { switch (result) { @@ -264,6 +266,11 @@ namespace StardewModdingAPI.Framework.ModLoading this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Found an incompatible CIL instruction ({handler.NounPhrase}) while loading assembly {filename}, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly.", LogLevel.Warn); break; + case InstructionHandleResult.DetectedGamePatch: + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected {handler.NounPhrase} in assembly {filename}."); + this.Monitor.LogOnce(loggedMessages, $"{mod.DisplayName} patches the game in a way that may impact game stability (detected {handler.NounPhrase}).", LogLevel.Warn); + break; + case InstructionHandleResult.None: break; diff --git a/src/StardewModdingAPI/Framework/ModLoading/InstructionHandleResult.cs b/src/StardewModdingAPI/Framework/ModLoading/InstructionHandleResult.cs index 3921e9c4..f4abb095 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/InstructionHandleResult.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/InstructionHandleResult.cs @@ -10,6 +10,9 @@ namespace StardewModdingAPI.Framework.ModLoading Rewritten, /// The instruction is not compatible and can't be rewritten for compatibility. - NotCompatible + NotCompatible, + + /// The instruction is compatible, but patches the game in a way that may impact stability. + DetectedGamePatch } } diff --git a/src/StardewModdingAPI/Metadata/InstructionMetadata.cs b/src/StardewModdingAPI/Metadata/InstructionMetadata.cs index fc4f112c..c53755ae 100644 --- a/src/StardewModdingAPI/Metadata/InstructionMetadata.cs +++ b/src/StardewModdingAPI/Metadata/InstructionMetadata.cs @@ -69,6 +69,11 @@ namespace StardewModdingAPI.Metadata new PropertyFinder("StardewModdingAPI.Mod", "PerSaveConfigPath", InstructionHandleResult.NotCompatible), #endif + /**** + ** detect code which may impact game stability + ****/ + new TypeFinder("Harmony.HarmonyInstance", InstructionHandleResult.DetectedGamePatch), + /**** ** rewrite CIL to fix incompatible code ****/ diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 175fc3f3..8da93bf4 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -110,7 +110,6 @@ - From 7b7176f6d824ae63cf996c73fffa811906c49cfb Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 20 Sep 2017 00:09:20 -0400 Subject: [PATCH 058/186] add missing release notes --- release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/release-notes.md b/release-notes.md index 6d430c7a..c31d2bae 100644 --- a/release-notes.md +++ b/release-notes.md @@ -7,6 +7,7 @@ For players: * The console now adjusts its colors when you have a light terminal background. * SMAPI now detects mods which may impact game stability and shows a warning in the console. * Updated compatibility list. +* Renamed installer folder from `SMAPI 2.0` to `SMAPI 2.0 installer` to avoid confusion. For mod developers: * Added new APIs to edit, inject, and reload XNB assets loaded by the game at any time. @@ -27,6 +28,9 @@ For mod developers: For power users: * Added command-line arguments to the SMAPI installer so it can be scripted. +For SMAPI developers: +* SMAPI has been significantly refactored under the hood to support changes in 2.0 and upcoming releases. + ## 1.15.4 For players: * Fixed errors when loading some custom maps on Linux/Mac or using XNB Loader. From 51269929587042bfd2c0ade67a1b3994c1e1285b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 20 Sep 2017 00:09:47 -0400 Subject: [PATCH 059/186] disable new ReSharper inspection --- src/StardewModdingAPI.sln.DotSettings | 1 + 1 file changed, 1 insertion(+) diff --git a/src/StardewModdingAPI.sln.DotSettings b/src/StardewModdingAPI.sln.DotSettings index 06cc66ef..d16ef684 100644 --- a/src/StardewModdingAPI.sln.DotSettings +++ b/src/StardewModdingAPI.sln.DotSettings @@ -1,4 +1,5 @@  + DO_NOT_SHOW DO_NOT_SHOW HINT HINT From 81c42ac77376746ac464a1b1708d08fcc5a55a16 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 20 Sep 2017 00:23:45 -0400 Subject: [PATCH 060/186] warn for mods which change the serialiser (#347) --- .../Framework/ModLoading/AssemblyLoader.cs | 9 +++++++-- .../Framework/ModLoading/InstructionHandleResult.cs | 5 ++++- src/StardewModdingAPI/Metadata/InstructionMetadata.cs | 3 +++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs index 835ef631..25bfb47e 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs @@ -267,8 +267,13 @@ namespace StardewModdingAPI.Framework.ModLoading break; case InstructionHandleResult.DetectedGamePatch: - this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected {handler.NounPhrase} in assembly {filename}."); - this.Monitor.LogOnce(loggedMessages, $"{mod.DisplayName} patches the game in a way that may impact game stability (detected {handler.NounPhrase}).", LogLevel.Warn); + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected game patcher ({handler.NounPhrase}) in assembly {filename}."); + this.Monitor.LogOnce(loggedMessages, $"{mod.DisplayName} patches the game, which may impact game stability. If you encounter problems, try removing this mod first.", LogLevel.Warn); + break; + + case InstructionHandleResult.DetectedSaveSerialiser: + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected possible save serialiser change ({handler.NounPhrase}) in assembly {filename}."); + this.Monitor.LogOnce(loggedMessages, $"{mod.DisplayName} seems to change the save serialiser. It may change your saves in such a way that they won't work without this mod in the future.", LogLevel.Warn); break; case InstructionHandleResult.None: diff --git a/src/StardewModdingAPI/Framework/ModLoading/InstructionHandleResult.cs b/src/StardewModdingAPI/Framework/ModLoading/InstructionHandleResult.cs index f4abb095..95d57ef2 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/InstructionHandleResult.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/InstructionHandleResult.cs @@ -13,6 +13,9 @@ namespace StardewModdingAPI.Framework.ModLoading NotCompatible, /// The instruction is compatible, but patches the game in a way that may impact stability. - DetectedGamePatch + DetectedGamePatch, + + /// The instruction is compatible, but affects the save serializer in a way that may make saves unloadable without the mod. + DetectedSaveSerialiser } } diff --git a/src/StardewModdingAPI/Metadata/InstructionMetadata.cs b/src/StardewModdingAPI/Metadata/InstructionMetadata.cs index c53755ae..79fabbd2 100644 --- a/src/StardewModdingAPI/Metadata/InstructionMetadata.cs +++ b/src/StardewModdingAPI/Metadata/InstructionMetadata.cs @@ -73,6 +73,9 @@ namespace StardewModdingAPI.Metadata ** detect code which may impact game stability ****/ new TypeFinder("Harmony.HarmonyInstance", InstructionHandleResult.DetectedGamePatch), + new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.serializer), InstructionHandleResult.DetectedSaveSerialiser), + new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.farmerSerializer), InstructionHandleResult.DetectedSaveSerialiser), + new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.locationSerializer), InstructionHandleResult.DetectedSaveSerialiser), /**** ** rewrite CIL to fix incompatible code From f0e2117f70455bd9883321ae5b0bf40562f2d5de Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 20 Sep 2017 00:33:36 -0400 Subject: [PATCH 061/186] remove mod metadata from instruction handlers, no longer needed (#347) --- .../Framework/ModLoading/AssemblyLoader.cs | 4 ++-- .../Framework/ModLoading/Finders/EventFinder.cs | 6 ++---- .../Framework/ModLoading/Finders/FieldFinder.cs | 6 ++---- .../Framework/ModLoading/Finders/MethodFinder.cs | 6 ++---- .../Framework/ModLoading/Finders/PropertyFinder.cs | 6 ++---- .../Framework/ModLoading/Finders/TypeFinder.cs | 6 ++---- .../Framework/ModLoading/IInstructionHandler.cs | 6 ++---- .../Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs | 3 +-- .../ModLoading/Rewriters/FieldToPropertyRewriter.cs | 3 +-- .../Framework/ModLoading/Rewriters/MethodParentRewriter.cs | 6 ++---- .../Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs | 6 ++---- 11 files changed, 20 insertions(+), 38 deletions(-) diff --git a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs index 25bfb47e..32988f97 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs @@ -221,7 +221,7 @@ namespace StardewModdingAPI.Framework.ModLoading // check method definition foreach (IInstructionHandler handler in handlers) { - InstructionHandleResult result = handler.Handle(mod, module, method, this.AssemblyMap, platformChanged); + InstructionHandleResult result = handler.Handle(module, method, this.AssemblyMap, platformChanged); this.ProcessInstructionHandleResult(mod, handler, result, loggedMessages, logPrefix, assumeCompatible, filename); if (result == InstructionHandleResult.Rewritten) anyRewritten = true; @@ -233,7 +233,7 @@ namespace StardewModdingAPI.Framework.ModLoading { foreach (IInstructionHandler handler in handlers) { - InstructionHandleResult result = handler.Handle(mod, module, cil, instruction, this.AssemblyMap, platformChanged); + InstructionHandleResult result = handler.Handle(module, cil, instruction, this.AssemblyMap, platformChanged); this.ProcessInstructionHandleResult(mod, handler, result, loggedMessages, logPrefix, assumeCompatible, filename); if (result == InstructionHandleResult.Rewritten) anyRewritten = true; diff --git a/src/StardewModdingAPI/Framework/ModLoading/Finders/EventFinder.cs b/src/StardewModdingAPI/Framework/ModLoading/Finders/EventFinder.cs index cd65b2dd..e4beb7a9 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/Finders/EventFinder.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/Finders/EventFinder.cs @@ -42,24 +42,22 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders } /// Perform the predefined logic for a method if applicable. - /// The mod containing the instruction. /// The assembly module containing the instruction. /// The method definition containing the instruction. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - public virtual InstructionHandleResult Handle(IModMetadata mod, ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) { return InstructionHandleResult.None; } /// Perform the predefined logic for an instruction if applicable. - /// The mod containing the instruction. /// The assembly module containing the instruction. /// The CIL processor. /// The instruction to handle. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - public virtual InstructionHandleResult Handle(IModMetadata mod, ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + public virtual InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { return this.IsMatch(instruction) ? this.Result diff --git a/src/StardewModdingAPI/Framework/ModLoading/Finders/FieldFinder.cs b/src/StardewModdingAPI/Framework/ModLoading/Finders/FieldFinder.cs index 1ec4483e..00805815 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/Finders/FieldFinder.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/Finders/FieldFinder.cs @@ -42,24 +42,22 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders } /// Perform the predefined logic for a method if applicable. - /// The mod containing the instruction. /// The assembly module containing the instruction. /// The method definition containing the instruction. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - public virtual InstructionHandleResult Handle(IModMetadata mod, ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) { return InstructionHandleResult.None; } /// Perform the predefined logic for an instruction if applicable. - /// The mod containing the instruction. /// The assembly module containing the instruction. /// The CIL processor. /// The instruction to handle. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - public virtual InstructionHandleResult Handle(IModMetadata mod, ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + public virtual InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { return this.IsMatch(instruction) ? this.Result diff --git a/src/StardewModdingAPI/Framework/ModLoading/Finders/MethodFinder.cs b/src/StardewModdingAPI/Framework/ModLoading/Finders/MethodFinder.cs index 4924c6e2..5358f181 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/Finders/MethodFinder.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/Finders/MethodFinder.cs @@ -42,24 +42,22 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders } /// Perform the predefined logic for a method if applicable. - /// The mod containing the instruction. /// The assembly module containing the instruction. /// The method definition containing the instruction. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - public virtual InstructionHandleResult Handle(IModMetadata mod, ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) { return InstructionHandleResult.None; } /// Perform the predefined logic for an instruction if applicable. - /// The mod containing the instruction. /// The assembly module containing the instruction. /// The CIL processor. /// The instruction to handle. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - public InstructionHandleResult Handle(IModMetadata mod, ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + public InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { return this.IsMatch(instruction) ? this.Result diff --git a/src/StardewModdingAPI/Framework/ModLoading/Finders/PropertyFinder.cs b/src/StardewModdingAPI/Framework/ModLoading/Finders/PropertyFinder.cs index 37392f77..e54c86cf 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/Finders/PropertyFinder.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/Finders/PropertyFinder.cs @@ -42,24 +42,22 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders } /// Perform the predefined logic for a method if applicable. - /// The mod containing the instruction. /// The assembly module containing the instruction. /// The method definition containing the instruction. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - public virtual InstructionHandleResult Handle(IModMetadata mod, ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) { return InstructionHandleResult.None; } /// Perform the predefined logic for an instruction if applicable. - /// The mod containing the instruction. /// The assembly module containing the instruction. /// The CIL processor. /// The instruction to handle. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - public virtual InstructionHandleResult Handle(IModMetadata mod, ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + public virtual InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { return this.IsMatch(instruction) ? this.Result diff --git a/src/StardewModdingAPI/Framework/ModLoading/Finders/TypeFinder.cs b/src/StardewModdingAPI/Framework/ModLoading/Finders/TypeFinder.cs index a0703669..45349def 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/Finders/TypeFinder.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/Finders/TypeFinder.cs @@ -38,12 +38,11 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders } /// Perform the predefined logic for a method if applicable. - /// The mod containing the instruction. /// The assembly module containing the instruction. /// The method definition containing the instruction. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - public virtual InstructionHandleResult Handle(IModMetadata mod, ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) { return this.IsMatch(method) ? this.Result @@ -51,13 +50,12 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders } /// Perform the predefined logic for an instruction if applicable. - /// The mod containing the instruction. /// The assembly module containing the instruction. /// The CIL processor. /// The instruction to handle. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - public virtual InstructionHandleResult Handle(IModMetadata mod, ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + public virtual InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { return this.IsMatch(instruction) ? this.Result diff --git a/src/StardewModdingAPI/Framework/ModLoading/IInstructionHandler.cs b/src/StardewModdingAPI/Framework/ModLoading/IInstructionHandler.cs index be2e10ee..8830cc74 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/IInstructionHandler.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/IInstructionHandler.cs @@ -17,20 +17,18 @@ namespace StardewModdingAPI.Framework.ModLoading ** Methods *********/ /// Perform the predefined logic for a method if applicable. - /// The mod containing the instruction. /// The assembly module containing the instruction. /// The method definition containing the instruction. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - InstructionHandleResult Handle(IModMetadata mod, ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged); + InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged); /// Perform the predefined logic for an instruction if applicable. - /// The mod containing the instruction. /// The assembly module containing the instruction. /// The CIL processor. /// The instruction to handle. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - InstructionHandleResult Handle(IModMetadata mod, ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged); + InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged); } } diff --git a/src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs index 324d1d1e..63358b39 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs @@ -32,13 +32,12 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters } /// Perform the predefined logic for an instruction if applicable. - /// The mod containing the instruction. /// The assembly module containing the instruction. /// The CIL processor. /// The instruction to handle. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - public override InstructionHandleResult Handle(IModMetadata mod, ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + public override InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { if (!this.IsMatch(instruction)) return InstructionHandleResult.None; diff --git a/src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs index 1b005869..a20b8bee 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs @@ -32,13 +32,12 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters } /// Perform the predefined logic for an instruction if applicable. - /// The mod containing the instruction. /// The assembly module containing the instruction. /// The CIL processor. /// The instruction to handle. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - public override InstructionHandleResult Handle(IModMetadata mod, ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + public override InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { if (!this.IsMatch(instruction)) return InstructionHandleResult.None; diff --git a/src/StardewModdingAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs index 50338071..974fcf4c 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs @@ -43,24 +43,22 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters } /// Perform the predefined logic for a method if applicable. - /// The mod containing the instruction. /// The assembly module containing the instruction. /// The method definition containing the instruction. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - public InstructionHandleResult Handle(IModMetadata mod, ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + public InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) { return InstructionHandleResult.None; } /// Perform the predefined logic for an instruction if applicable. - /// The mod containing the instruction. /// The assembly module containing the instruction. /// The CIL processor. /// The instruction to handle. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - public InstructionHandleResult Handle(IModMetadata mod, ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + public InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { if (!this.IsMatch(instruction, platformChanged)) return InstructionHandleResult.None; diff --git a/src/StardewModdingAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs index df6e5e4b..74f2fcdd 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs @@ -32,12 +32,11 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters } /// Perform the predefined logic for a method if applicable. - /// The mod containing the instruction. /// The assembly module containing the instruction. /// The method definition containing the instruction. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - public override InstructionHandleResult Handle(IModMetadata mod, ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + public override InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) { bool rewritten = false; @@ -87,13 +86,12 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters } /// Perform the predefined logic for an instruction if applicable. - /// The mod containing the instruction. /// The assembly module containing the instruction. /// The CIL processor. /// The instruction to handle. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - public override InstructionHandleResult Handle(IModMetadata mod, ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + public override InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { if (!this.IsMatch(instruction) && !instruction.ToString().Contains(this.FromTypeName)) return InstructionHandleResult.None; From 9791de306c22c744732219dadfd97b7dd556a5b2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 21 Sep 2017 23:35:18 -0400 Subject: [PATCH 062/186] minor cleanup, formatting, documentation (#336) --- Dewdrop.sln | 22 ------------ Dewdrop/Controllers/CheckController.cs | 8 ++++- Dewdrop/Models/IModModel.cs | 18 ++++------ Dewdrop/Models/ModGenericModel.cs | 37 +++++++------------- Dewdrop/Models/NexusResponseModel.cs | 47 +++++++++++--------------- Dewdrop/Program.cs | 22 ++++++------ Dewdrop/Startup.cs | 39 +++++++++++++-------- src/StardewModdingAPI.sln | 17 ++++++++++ 8 files changed, 98 insertions(+), 112 deletions(-) delete mode 100644 Dewdrop.sln diff --git a/Dewdrop.sln b/Dewdrop.sln deleted file mode 100644 index 761e6ccc..00000000 --- a/Dewdrop.sln +++ /dev/null @@ -1,22 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26430.16 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dewdrop", "Dewdrop\Dewdrop.csproj", "{0DEC7A86-4D43-43B0-A5F8-7686DDB6EC97}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {0DEC7A86-4D43-43B0-A5F8-7686DDB6EC97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0DEC7A86-4D43-43B0-A5F8-7686DDB6EC97}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0DEC7A86-4D43-43B0-A5F8-7686DDB6EC97}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0DEC7A86-4D43-43B0-A5F8-7686DDB6EC97}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal diff --git a/Dewdrop/Controllers/CheckController.cs b/Dewdrop/Controllers/CheckController.cs index f3cdd364..def2edbc 100644 --- a/Dewdrop/Controllers/CheckController.cs +++ b/Dewdrop/Controllers/CheckController.cs @@ -8,10 +8,16 @@ using Dewdrop.Models; namespace Dewdrop.Controllers { + /// Provides an API to perform mod update checks. [Produces("application/json")] [Route("api/check")] public class CheckController : Controller { + /********* + ** Public methods + *********/ + /// Fetch version metadata for the given mods. + /// The mods for which to fetch update metadata. [HttpPost] public async Task Post([FromBody] NexusResponseModel[] mods) { @@ -25,7 +31,7 @@ namespace Dewdrop.Controllers try { // create request with HttpRequestMessage - var request = new HttpRequestMessage(HttpMethod.Get, new Uri($"http://www.nexusmods.com/stardewvalley/mods/{mod.Id}")); + var request = new HttpRequestMessage(HttpMethod.Get, new Uri($"http://www.nexusmods.com/stardewvalley/mods/{mod.ID}")); // add the Nexus Client useragent to get JSON response from the site request.Headers.UserAgent.ParseAdd("Nexus Client v0.63.15"); diff --git a/Dewdrop/Models/IModModel.cs b/Dewdrop/Models/IModModel.cs index aa6583d4..f1b09f8a 100644 --- a/Dewdrop/Models/IModModel.cs +++ b/Dewdrop/Models/IModModel.cs @@ -1,16 +1,12 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Dewdrop.Models +namespace Dewdrop.Models { - interface IModModel + /// A mod metadata response which provides a method to extract generic info. + internal interface IModModel { - /// - /// Basic information in the form of - /// - /// + /********* + ** Public methods + *********/ + /// Get basic mod metadata. ModGenericModel ModInfo(); } } diff --git a/Dewdrop/Models/ModGenericModel.cs b/Dewdrop/Models/ModGenericModel.cs index 829c396a..0da04295 100644 --- a/Dewdrop/Models/ModGenericModel.cs +++ b/Dewdrop/Models/ModGenericModel.cs @@ -1,40 +1,27 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Dewdrop.Models +namespace Dewdrop.Models { + /// Generic metadata about a mod. public class ModGenericModel { - /// - /// An identifier for the mod. - /// - public int Id { get; set; } + /********* + ** Accessors + *********/ + /// The unique mod ID. + public int ID { get; set; } - /// - /// The mod's name. - /// + /// The mod name. public string Name { get; set; } - /// - /// The vendor identifier for the mod. - /// + /// The mod's vendor ID. public string Vendor { get; set; } - /// - /// The mod's version number. - /// + /// The mod's semantic version number. public string Version { get; set; } - /// - /// The mod's URL - /// + /// The mod's web URL. public string Url { get; set; } - /// - /// Is the mod a valid mod. - /// + /// Whether the mod is valid. public bool Valid { get; set; } = true; } } diff --git a/Dewdrop/Models/NexusResponseModel.cs b/Dewdrop/Models/NexusResponseModel.cs index e954a8bc..fa663910 100644 --- a/Dewdrop/Models/NexusResponseModel.cs +++ b/Dewdrop/Models/NexusResponseModel.cs @@ -1,46 +1,39 @@ -using System; -using Newtonsoft.Json; +using Newtonsoft.Json; namespace Dewdrop.Models { + /// A mod metadata response from Nexus Mods. public class NexusResponseModel : IModModel { - /// - /// The name of the mod. - /// - [JsonProperty("name")] + /********* + ** Accessors + *********/ + /// The unique mod ID. + public int ID { get; set; } + + /// The mod name. public string Name { get; set; } - /// - /// The version of the mod. - /// - [JsonProperty("version")] + /// The mod's semantic version number. public string Version { get; set; } - /// - /// The NexusMod ID for the mod. - /// - [JsonProperty("id")] - public int Id { get; set; } - - /// - /// The URL of the mod. - /// + /// The mod's web URL. [JsonProperty("mod_page_uri")] public string Url { get; set; } - /// - /// Return mod information about a Nexus mod - /// - /// + + /********* + ** Public methods + *********/ + /// Get basic mod metadata. public ModGenericModel ModInfo() { return new ModGenericModel { - Id = Id, - Version = Version, - Name = Name, - Url = Url, + ID = this.ID, + Version = this.Version, + Name = this.Name, + Url = this.Url, Vendor = "Nexus" }; } diff --git a/Dewdrop/Program.cs b/Dewdrop/Program.cs index c6a5a642..0e831a23 100644 --- a/Dewdrop/Program.cs +++ b/Dewdrop/Program.cs @@ -1,27 +1,27 @@ -using System; -using System.IO; -using System.Net.Http; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; +using System.IO; using Microsoft.AspNetCore.Hosting; -using Newtonsoft.Json; -using System.Collections.Generic; namespace Dewdrop { + /// The main app entry point. public class Program { + /********* + ** Public methods + *********/ + /// The main app entry point. + /// The command-line arguments. public static void Main(string[] args) { - var host = new WebHostBuilder() + // configure web server + new WebHostBuilder() .UseKestrel() .UseContentRoot(Directory.GetCurrentDirectory()) .UseIISIntegration() .UseStartup() .UseApplicationInsights() - .Build(); - - host.Run(); + .Build() + .Run(); } } } diff --git a/Dewdrop/Startup.cs b/Dewdrop/Startup.cs index 00c18bab..d1618037 100644 --- a/Dewdrop/Startup.cs +++ b/Dewdrop/Startup.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -10,33 +6,46 @@ using Microsoft.Extensions.Logging; namespace Dewdrop { + /// The web app startup configuration. public class Startup { + /********* + ** Accessors + *********/ + /// The web app configuration. + public IConfigurationRoot Configuration { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The hosting environment. public Startup(IHostingEnvironment env) { - var builder = new ConfigurationBuilder() + this.Configuration = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) - .AddEnvironmentVariables(); - Configuration = builder.Build(); + .AddEnvironmentVariables() + .Build(); } - public IConfigurationRoot Configuration { get; } - - // This method gets called by the runtime. Use this method to add services to the container. + /// The method called by the runtime to add services to the container. + /// The service injection container. public void ConfigureServices(IServiceCollection services) { - // Add framework services. services.AddMvc(); } - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + /// The method called by the runtime to configure the HTTP request pipeline. + /// The application builder. + /// The hosting environment. + /// The logger factory. public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { - loggerFactory.AddConsole(Configuration.GetSection("Logging")); + loggerFactory.AddConsole(this.Configuration.GetSection("Logging")); loggerFactory.AddDebug(); - app.UseMvc(); } } diff --git a/src/StardewModdingAPI.sln b/src/StardewModdingAPI.sln index a2e0ec44..3cf129c0 100644 --- a/src/StardewModdingAPI.sln +++ b/src/StardewModdingAPI.sln @@ -29,6 +29,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Installer EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Tests", "StardewModdingAPI.Tests\StardewModdingAPI.Tests.csproj", "{36CCB19E-92EB-48C7-9615-98EEFD45109B}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dewdrop", "..\Dewdrop\Dewdrop.csproj", "{A308F679-51A3-4006-92D5-BAEC7EBD01A1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -79,8 +81,23 @@ Global {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Release|Mixed Platforms.Build.0 = Release|x86 {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Release|x86.ActiveCfg = Release|x86 {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Release|x86.Build.0 = Release|x86 + {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|x86.ActiveCfg = Debug|Any CPU + {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|x86.Build.0 = Debug|Any CPU + {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|Any CPU.Build.0 = Release|Any CPU + {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|x86.ActiveCfg = Release|Any CPU + {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {70143042-A862-47A8-A677-7C819DDC90DC} + EndGlobalSection EndGlobal From cddcd9a8cfde182e843f8b2224d00ba742596c76 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 21 Sep 2017 23:39:12 -0400 Subject: [PATCH 063/186] standardise project name (#336) --- .../Controllers/CheckController.cs | 14 +++++++------- .../StardewModdingAPI.Web}/Models/IModModel.cs | 2 +- .../Models/ModGenericModel.cs | 2 +- .../Models/NexusResponseModel.cs | 4 ++-- {Dewdrop => src/StardewModdingAPI.Web}/Program.cs | 2 +- .../Properties/launchSettings.json | 0 .../StardewModdingAPI.Web.csproj | 0 {Dewdrop => src/StardewModdingAPI.Web}/Startup.cs | 2 +- .../appsettings.Development.json | 0 .../StardewModdingAPI.Web}/appsettings.json | 0 src/StardewModdingAPI.sln | 2 +- 11 files changed, 14 insertions(+), 14 deletions(-) rename {Dewdrop => src/StardewModdingAPI.Web}/Controllers/CheckController.cs (96%) rename {Dewdrop => src/StardewModdingAPI.Web}/Models/IModModel.cs (88%) rename {Dewdrop => src/StardewModdingAPI.Web}/Models/ModGenericModel.cs (95%) rename {Dewdrop => src/StardewModdingAPI.Web}/Models/NexusResponseModel.cs (94%) rename {Dewdrop => src/StardewModdingAPI.Web}/Program.cs (95%) rename {Dewdrop => src/StardewModdingAPI.Web}/Properties/launchSettings.json (100%) rename Dewdrop/Dewdrop.csproj => src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj (100%) rename {Dewdrop => src/StardewModdingAPI.Web}/Startup.cs (98%) rename {Dewdrop => src/StardewModdingAPI.Web}/appsettings.Development.json (100%) rename {Dewdrop => src/StardewModdingAPI.Web}/appsettings.json (100%) diff --git a/Dewdrop/Controllers/CheckController.cs b/src/StardewModdingAPI.Web/Controllers/CheckController.cs similarity index 96% rename from Dewdrop/Controllers/CheckController.cs rename to src/StardewModdingAPI.Web/Controllers/CheckController.cs index def2edbc..8ab4611b 100644 --- a/Dewdrop/Controllers/CheckController.cs +++ b/src/StardewModdingAPI.Web/Controllers/CheckController.cs @@ -1,12 +1,12 @@ using System; -using System.Net.Http; -using Microsoft.AspNetCore.Mvc; -using System.Threading.Tasks; -using Newtonsoft.Json; using System.Collections.Generic; -using Dewdrop.Models; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; +using StardewModdingAPI.Web.Models; -namespace Dewdrop.Controllers +namespace StardewModdingAPI.Web.Controllers { /// Provides an API to perform mod update checks. [Produces("application/json")] @@ -63,4 +63,4 @@ namespace Dewdrop.Controllers } } } -} \ No newline at end of file +} diff --git a/Dewdrop/Models/IModModel.cs b/src/StardewModdingAPI.Web/Models/IModModel.cs similarity index 88% rename from Dewdrop/Models/IModModel.cs rename to src/StardewModdingAPI.Web/Models/IModModel.cs index f1b09f8a..2eadcaec 100644 --- a/Dewdrop/Models/IModModel.cs +++ b/src/StardewModdingAPI.Web/Models/IModModel.cs @@ -1,4 +1,4 @@ -namespace Dewdrop.Models +namespace StardewModdingAPI.Web.Models { /// A mod metadata response which provides a method to extract generic info. internal interface IModModel diff --git a/Dewdrop/Models/ModGenericModel.cs b/src/StardewModdingAPI.Web/Models/ModGenericModel.cs similarity index 95% rename from Dewdrop/Models/ModGenericModel.cs rename to src/StardewModdingAPI.Web/Models/ModGenericModel.cs index 0da04295..208af416 100644 --- a/Dewdrop/Models/ModGenericModel.cs +++ b/src/StardewModdingAPI.Web/Models/ModGenericModel.cs @@ -1,4 +1,4 @@ -namespace Dewdrop.Models +namespace StardewModdingAPI.Web.Models { /// Generic metadata about a mod. public class ModGenericModel diff --git a/Dewdrop/Models/NexusResponseModel.cs b/src/StardewModdingAPI.Web/Models/NexusResponseModel.cs similarity index 94% rename from Dewdrop/Models/NexusResponseModel.cs rename to src/StardewModdingAPI.Web/Models/NexusResponseModel.cs index fa663910..ae5c691c 100644 --- a/Dewdrop/Models/NexusResponseModel.cs +++ b/src/StardewModdingAPI.Web/Models/NexusResponseModel.cs @@ -1,6 +1,6 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; -namespace Dewdrop.Models +namespace StardewModdingAPI.Web.Models { /// A mod metadata response from Nexus Mods. public class NexusResponseModel : IModModel diff --git a/Dewdrop/Program.cs b/src/StardewModdingAPI.Web/Program.cs similarity index 95% rename from Dewdrop/Program.cs rename to src/StardewModdingAPI.Web/Program.cs index 0e831a23..5e258acc 100644 --- a/Dewdrop/Program.cs +++ b/src/StardewModdingAPI.Web/Program.cs @@ -1,7 +1,7 @@ using System.IO; using Microsoft.AspNetCore.Hosting; -namespace Dewdrop +namespace StardewModdingAPI.Web { /// The main app entry point. public class Program diff --git a/Dewdrop/Properties/launchSettings.json b/src/StardewModdingAPI.Web/Properties/launchSettings.json similarity index 100% rename from Dewdrop/Properties/launchSettings.json rename to src/StardewModdingAPI.Web/Properties/launchSettings.json diff --git a/Dewdrop/Dewdrop.csproj b/src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj similarity index 100% rename from Dewdrop/Dewdrop.csproj rename to src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj diff --git a/Dewdrop/Startup.cs b/src/StardewModdingAPI.Web/Startup.cs similarity index 98% rename from Dewdrop/Startup.cs rename to src/StardewModdingAPI.Web/Startup.cs index d1618037..c7a5e8fe 100644 --- a/Dewdrop/Startup.cs +++ b/src/StardewModdingAPI.Web/Startup.cs @@ -4,7 +4,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Dewdrop +namespace StardewModdingAPI.Web { /// The web app startup configuration. public class Startup diff --git a/Dewdrop/appsettings.Development.json b/src/StardewModdingAPI.Web/appsettings.Development.json similarity index 100% rename from Dewdrop/appsettings.Development.json rename to src/StardewModdingAPI.Web/appsettings.Development.json diff --git a/Dewdrop/appsettings.json b/src/StardewModdingAPI.Web/appsettings.json similarity index 100% rename from Dewdrop/appsettings.json rename to src/StardewModdingAPI.Web/appsettings.json diff --git a/src/StardewModdingAPI.sln b/src/StardewModdingAPI.sln index 3cf129c0..031e2d5f 100644 --- a/src/StardewModdingAPI.sln +++ b/src/StardewModdingAPI.sln @@ -29,7 +29,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Installer EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Tests", "StardewModdingAPI.Tests\StardewModdingAPI.Tests.csproj", "{36CCB19E-92EB-48C7-9615-98EEFD45109B}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dewdrop", "..\Dewdrop\Dewdrop.csproj", "{A308F679-51A3-4006-92D5-BAEC7EBD01A1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StardewModdingAPI.Web", "StardewModdingAPI.Web\StardewModdingAPI.Web.csproj", "{A308F679-51A3-4006-92D5-BAEC7EBD01A1}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution From 1f32d72099b16a554c4c79f499f678ebd6a904a9 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 21 Sep 2017 23:39:20 -0400 Subject: [PATCH 064/186] simplify .gitignore --- .gitignore | 249 ++--------------------------------------------------- 1 file changed, 5 insertions(+), 244 deletions(-) diff --git a/.gitignore b/.gitignore index 91b9bce3..f2d50778 100644 --- a/.gitignore +++ b/.gitignore @@ -1,264 +1,25 @@ -# SMAPI Specific Ignores -StardewModdingAPI/bin/ -StardewModdingAPI/obj/ -TrainerMod/bin/ -TrainerMod/obj/ -StardewInjector/bin/ -StardewInjector/obj/ -packages/ -steamapps/ - -*.symlink -*.lnk -!*.exe -!*.dll - -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. - -# User-specific files +# user-specific files *.suo *.user *.userosscache *.sln.docstates -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Build results +# build results [Dd]ebug/ -[Dd]ebugPublic/ [Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -bld/ [Bb]in/ [Oo]bj/ -[Ll]og/ -# Visual Studio 2015 cache/options directory +# Visual Studio cache/options .vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUNIT -*.VisualState.xml -TestResult.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# DNX -project.lock.json -artifacts/ - -*_i.c -*_p.c -*_i.h -*.ilk -*.meta -*.obj -*.pch -*.pdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in +# ReSharper _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user -# JustCode is a .NET coding add-in -.JustCode - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# TODO: Comment the next line if you want to checkin your web deploy settings -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# NuGet Packages +# NuGet packages *.nupkg -# The packages folder can be ignored because of Package Restore **/packages/* -# except build/, which is used as an MSBuild target. -!**/packages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/packages/repositories.config -# NuGet v3's project.json files produces more ignoreable files *.nuget.props *.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Microsoft Azure ApplicationInsights config file -ApplicationInsights.config - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.pfx -*.publishsettings -node_modules/ -orleans.codegen.cs - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm - -# SQL Server files -*.mdf -*.ldf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe - -# FAKE - F# Make -.fake/ - -# JetBrains Rider -.idea/ -*.sln.iml - -Dewdrop/ScaffoldingReadMe.txt From 1bd59fc1d82d6d6316812a5609f61c4157af78c3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 21 Sep 2017 23:48:47 -0400 Subject: [PATCH 065/186] split input model from Nexus response model (#336) --- .../Controllers/CheckController.cs | 16 ++++++++-------- .../Models/ModSearchModel.cs | 9 +++++++++ 2 files changed, 17 insertions(+), 8 deletions(-) create mode 100644 src/StardewModdingAPI.Web/Models/ModSearchModel.cs diff --git a/src/StardewModdingAPI.Web/Controllers/CheckController.cs b/src/StardewModdingAPI.Web/Controllers/CheckController.cs index 8ab4611b..47b61b2f 100644 --- a/src/StardewModdingAPI.Web/Controllers/CheckController.cs +++ b/src/StardewModdingAPI.Web/Controllers/CheckController.cs @@ -19,7 +19,7 @@ namespace StardewModdingAPI.Web.Controllers /// Fetch version metadata for the given mods. /// The mods for which to fetch update metadata. [HttpPost] - public async Task Post([FromBody] NexusResponseModel[] mods) + public async Task Post([FromBody] ModSearchModel[] mods) { using (var client = new HttpClient()) { @@ -28,10 +28,13 @@ namespace StardewModdingAPI.Web.Controllers foreach (var mod in mods) { + if (!mod.NexusID.HasValue) + continue; + try { // create request with HttpRequestMessage - var request = new HttpRequestMessage(HttpMethod.Get, new Uri($"http://www.nexusmods.com/stardewvalley/mods/{mod.ID}")); + var request = new HttpRequestMessage(HttpMethod.Get, new Uri($"http://www.nexusmods.com/stardewvalley/mods/{mod.NexusID}")); // add the Nexus Client useragent to get JSON response from the site request.Headers.UserAgent.ParseAdd("Nexus Client v0.63.15"); @@ -42,7 +45,7 @@ namespace StardewModdingAPI.Web.Controllers response.EnsureSuccessStatusCode(); // get the JSON string of the response - var stringResponse = await response.Content.ReadAsStringAsync(); + string stringResponse = await response.Content.ReadAsStringAsync(); // create the mod data from the JSON string var modData = JsonConvert.DeserializeObject(stringResponse); @@ -50,12 +53,9 @@ namespace StardewModdingAPI.Web.Controllers // add to the list of mods modList.Add(modData.ModInfo()); } - catch (Exception ex) + catch (Exception) { - var modData = mod.ModInfo(); - modData.Valid = false; - - modList.Add(modData); + modList.Add(new ModGenericModel { ID = mod.NexusID.Value, Vendor = "Nexus", Valid = false }); } } diff --git a/src/StardewModdingAPI.Web/Models/ModSearchModel.cs b/src/StardewModdingAPI.Web/Models/ModSearchModel.cs new file mode 100644 index 00000000..eb9ac920 --- /dev/null +++ b/src/StardewModdingAPI.Web/Models/ModSearchModel.cs @@ -0,0 +1,9 @@ +namespace StardewModdingAPI.Web.Models +{ + /// The search criteria for a mod. + public class ModSearchModel + { + /// The Nexus Mods ID (if any). + public int? NexusID { get; set; } + } +} From dfae52b1e57677c1efa052b1073a2e8a56f32163 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 21 Sep 2017 23:50:34 -0400 Subject: [PATCH 066/186] fix manual serialisation (#336) --- src/StardewModdingAPI.Web/Controllers/CheckController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/StardewModdingAPI.Web/Controllers/CheckController.cs b/src/StardewModdingAPI.Web/Controllers/CheckController.cs index 47b61b2f..a7582edd 100644 --- a/src/StardewModdingAPI.Web/Controllers/CheckController.cs +++ b/src/StardewModdingAPI.Web/Controllers/CheckController.cs @@ -19,7 +19,7 @@ namespace StardewModdingAPI.Web.Controllers /// Fetch version metadata for the given mods. /// The mods for which to fetch update metadata. [HttpPost] - public async Task Post([FromBody] ModSearchModel[] mods) + public async Task Post([FromBody] ModSearchModel[] mods) { using (var client = new HttpClient()) { @@ -59,7 +59,7 @@ namespace StardewModdingAPI.Web.Controllers } } - return JsonConvert.SerializeObject(modList, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); + return modList.ToArray(); } } } From edbc3ef3c08909486d6c4b8797f0e3c2934854fa Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 22 Sep 2017 00:13:04 -0400 Subject: [PATCH 067/186] refactor Nexus code into generic vendor, rewrite using fluent HTTP client (#336) --- .../Controllers/CheckController.cs | 46 +++--------- .../Framework/IModRepository.cs | 17 +++++ .../Framework/NexusModsClient.cs | 75 +++++++++++++++++++ src/StardewModdingAPI.Web/Models/IModModel.cs | 12 --- .../Models/ModGenericModel.cs | 43 +++++++++-- .../Models/NexusResponseModel.cs | 41 ---------- .../StardewModdingAPI.Web.csproj | 1 + 7 files changed, 139 insertions(+), 96 deletions(-) create mode 100644 src/StardewModdingAPI.Web/Framework/IModRepository.cs create mode 100644 src/StardewModdingAPI.Web/Framework/NexusModsClient.cs delete mode 100644 src/StardewModdingAPI.Web/Models/IModModel.cs delete mode 100644 src/StardewModdingAPI.Web/Models/NexusResponseModel.cs diff --git a/src/StardewModdingAPI.Web/Controllers/CheckController.cs b/src/StardewModdingAPI.Web/Controllers/CheckController.cs index a7582edd..9ec068cb 100644 --- a/src/StardewModdingAPI.Web/Controllers/CheckController.cs +++ b/src/StardewModdingAPI.Web/Controllers/CheckController.cs @@ -1,9 +1,7 @@ -using System; using System.Collections.Generic; -using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; -using Newtonsoft.Json; +using StardewModdingAPI.Web.Framework; using StardewModdingAPI.Web.Models; namespace StardewModdingAPI.Web.Controllers @@ -21,45 +19,19 @@ namespace StardewModdingAPI.Web.Controllers [HttpPost] public async Task Post([FromBody] ModSearchModel[] mods) { - using (var client = new HttpClient()) + using (NexusModsClient client = new NexusModsClient()) { - // the return array of mods - var modList = new List(); + List result = new List(); - foreach (var mod in mods) + foreach (ModSearchModel mod in mods) { - if (!mod.NexusID.HasValue) - continue; - - try - { - // create request with HttpRequestMessage - var request = new HttpRequestMessage(HttpMethod.Get, new Uri($"http://www.nexusmods.com/stardewvalley/mods/{mod.NexusID}")); - - // add the Nexus Client useragent to get JSON response from the site - request.Headers.UserAgent.ParseAdd("Nexus Client v0.63.15"); - - // send the request out - var response = await client.SendAsync(request); - // ensure the response is valid (throws exception) - response.EnsureSuccessStatusCode(); - - // get the JSON string of the response - string stringResponse = await response.Content.ReadAsStringAsync(); - - // create the mod data from the JSON string - var modData = JsonConvert.DeserializeObject(stringResponse); - - // add to the list of mods - modList.Add(modData.ModInfo()); - } - catch (Exception) - { - modList.Add(new ModGenericModel { ID = mod.NexusID.Value, Vendor = "Nexus", Valid = false }); - } + if (mod.NexusID.HasValue) + result.Add(await client.GetModInfoAsync(mod.NexusID.Value)); + else + result.Add(new ModGenericModel(null, mod.NexusID ?? 0)); } - return modList.ToArray(); + return result.ToArray(); } } } diff --git a/src/StardewModdingAPI.Web/Framework/IModRepository.cs b/src/StardewModdingAPI.Web/Framework/IModRepository.cs new file mode 100644 index 00000000..ebf9850f --- /dev/null +++ b/src/StardewModdingAPI.Web/Framework/IModRepository.cs @@ -0,0 +1,17 @@ +using System; +using System.Threading.Tasks; +using StardewModdingAPI.Web.Models; + +namespace StardewModdingAPI.Web.Framework +{ + /// A repository which provides mod metadata. + internal interface IModRepository : IDisposable + { + /********* + ** Public methods + *********/ + /// Get metadata about a mod in the repository. + /// The mod ID in this repository. + Task GetModInfoAsync(int id); + } +} diff --git a/src/StardewModdingAPI.Web/Framework/NexusModsClient.cs b/src/StardewModdingAPI.Web/Framework/NexusModsClient.cs new file mode 100644 index 00000000..8f010d56 --- /dev/null +++ b/src/StardewModdingAPI.Web/Framework/NexusModsClient.cs @@ -0,0 +1,75 @@ +using System; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Pathoschild.Http.Client; +using StardewModdingAPI.Web.Models; + +namespace StardewModdingAPI.Web.Framework +{ + /// An HTTP client for fetching mod metadata from Nexus Mods. + internal class NexusModsClient : IModRepository + { + /********* + ** Properties + *********/ + /// The underlying HTTP client. + private readonly IClient Client; + + /********* + ** Public methods + *********/ + /// Construct an instance. + public NexusModsClient() + { + this.Client = new FluentClient("http://www.nexusmods.com/stardewvalley") + .SetUserAgent("Nexus Client v0.63.15"); + } + + /// Get metadata about a mod in the repository. + /// The mod ID in this repository. + public async Task GetModInfoAsync(int id) + { + try + { + NexusResponseModel response = await this.Client + .GetAsync($"mods/{id}") + .As(); + return new ModGenericModel("Nexus", id, response.Name, response.Version, response.Url); + } + catch (Exception) + { + return new ModGenericModel("Nexus", id); + } + } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public void Dispose() + { + this.Client.Dispose(); + } + + + /********* + ** Private models + *********/ + /// A mod metadata response from Nexus Mods. + private class NexusResponseModel + { + /********* + ** Accessors + *********/ + /// The unique mod ID. + public int ID { get; set; } + + /// The mod name. + public string Name { get; set; } + + /// The mod's semantic version number. + public string Version { get; set; } + + /// The mod's web URL. + [JsonProperty("mod_page_uri")] + public string Url { get; set; } + } + } +} diff --git a/src/StardewModdingAPI.Web/Models/IModModel.cs b/src/StardewModdingAPI.Web/Models/IModModel.cs deleted file mode 100644 index 2eadcaec..00000000 --- a/src/StardewModdingAPI.Web/Models/IModModel.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace StardewModdingAPI.Web.Models -{ - /// A mod metadata response which provides a method to extract generic info. - internal interface IModModel - { - /********* - ** Public methods - *********/ - /// Get basic mod metadata. - ModGenericModel ModInfo(); - } -} diff --git a/src/StardewModdingAPI.Web/Models/ModGenericModel.cs b/src/StardewModdingAPI.Web/Models/ModGenericModel.cs index 208af416..dc36c7f4 100644 --- a/src/StardewModdingAPI.Web/Models/ModGenericModel.cs +++ b/src/StardewModdingAPI.Web/Models/ModGenericModel.cs @@ -7,21 +7,52 @@ namespace StardewModdingAPI.Web.Models ** Accessors *********/ /// The unique mod ID. - public int ID { get; set; } + public int ID { get; } /// The mod name. - public string Name { get; set; } + public string Name { get; } /// The mod's vendor ID. - public string Vendor { get; set; } + public string Vendor { get; } /// The mod's semantic version number. - public string Version { get; set; } + public string Version { get; } /// The mod's web URL. - public string Url { get; set; } + public string Url { get; } /// Whether the mod is valid. - public bool Valid { get; set; } = true; + public bool Valid { get; } + + + /********* + ** Public methods + *********/ + /// Construct a valid instance. + /// The mod's vendor ID. + /// The unique mod ID. + /// The mod name. + /// The mod's semantic version number. + /// The mod's web URL. + /// Whether the mod is valid. + public ModGenericModel(string vendor, int id, string name, string version, string url, bool valid = true) + { + this.Vendor = vendor; + this.ID = id; + this.Name = name; + this.Version = version; + this.Url = url; + this.Valid = valid; + } + + /// Construct an valid instance. + /// The mod's vendor ID. + /// The unique mod ID. + public ModGenericModel(string vendor, int id) + { + this.Vendor = vendor; + this.ID = id; + this.Valid = false; + } } } diff --git a/src/StardewModdingAPI.Web/Models/NexusResponseModel.cs b/src/StardewModdingAPI.Web/Models/NexusResponseModel.cs deleted file mode 100644 index ae5c691c..00000000 --- a/src/StardewModdingAPI.Web/Models/NexusResponseModel.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Newtonsoft.Json; - -namespace StardewModdingAPI.Web.Models -{ - /// A mod metadata response from Nexus Mods. - public class NexusResponseModel : IModModel - { - /********* - ** Accessors - *********/ - /// The unique mod ID. - public int ID { get; set; } - - /// The mod name. - public string Name { get; set; } - - /// The mod's semantic version number. - public string Version { get; set; } - - /// The mod's web URL. - [JsonProperty("mod_page_uri")] - public string Url { get; set; } - - - /********* - ** Public methods - *********/ - /// Get basic mod metadata. - public ModGenericModel ModInfo() - { - return new ModGenericModel - { - ID = this.ID, - Version = this.Version, - Name = this.Name, - Url = this.Url, - Vendor = "Nexus" - }; - } - } -} diff --git a/src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj b/src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj index fa1d88eb..eee0f3f3 100644 --- a/src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj +++ b/src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj @@ -19,6 +19,7 @@ + From 2c02dfe45a6d1252bfef557db8f39f97e57d3d19 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 22 Sep 2017 00:47:46 -0400 Subject: [PATCH 068/186] rewrite to make update-check logic vendor-agnostic (#336) --- .../Controllers/CheckController.cs | 73 ++++++++++++++++--- .../{ => ModRepositories}/IModRepository.cs | 11 ++- .../NexusRepository.cs} | 25 ++++--- .../Models/ModGenericModel.cs | 37 +++++----- .../Models/ModSearchModel.cs | 6 +- 5 files changed, 105 insertions(+), 47 deletions(-) rename src/StardewModdingAPI.Web/Framework/{ => ModRepositories}/IModRepository.cs (60%) rename src/StardewModdingAPI.Web/Framework/{NexusModsClient.cs => ModRepositories/NexusRepository.cs} (75%) diff --git a/src/StardewModdingAPI.Web/Controllers/CheckController.cs b/src/StardewModdingAPI.Web/Controllers/CheckController.cs index 9ec068cb..2a346cf3 100644 --- a/src/StardewModdingAPI.Web/Controllers/CheckController.cs +++ b/src/StardewModdingAPI.Web/Controllers/CheckController.cs @@ -1,7 +1,9 @@ +using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; -using StardewModdingAPI.Web.Framework; +using StardewModdingAPI.Web.Framework.ModRepositories; using StardewModdingAPI.Web.Models; namespace StardewModdingAPI.Web.Controllers @@ -11,28 +13,75 @@ namespace StardewModdingAPI.Web.Controllers [Route("api/check")] public class CheckController : Controller { + /********* + ** Properties + *********/ + /// The mod repositories which provide mod metadata. + private readonly IDictionary Repositories = + new IModRepository[] + { + new NexusRepository() + } + .ToDictionary(p => p.VendorKey, StringComparer.CurrentCultureIgnoreCase); + + /********* ** Public methods *********/ /// Fetch version metadata for the given mods. - /// The mods for which to fetch update metadata. + /// The mod update search criteria. [HttpPost] - public async Task Post([FromBody] ModSearchModel[] mods) + public async Task Post([FromBody] ModSearchModel search) { - using (NexusModsClient client = new NexusModsClient()) - { - List result = new List(); + IList result = new List(); - foreach (ModSearchModel mod in mods) + foreach (string modKey in search.ModKeys) + { + // parse mod key + if (!this.TryParseModKey(modKey, out string vendorKey, out string modID)) { - if (mod.NexusID.HasValue) - result.Add(await client.GetModInfoAsync(mod.NexusID.Value)); - else - result.Add(new ModGenericModel(null, mod.NexusID ?? 0)); + result.Add(new ModGenericModel(modKey, "The mod key isn't in a valid format. It should contain the mod repository key and mod ID like 'Nexus:541'.")); + continue; } - return result.ToArray(); + // get matching repository + if (!this.Repositories.TryGetValue(vendorKey, out IModRepository repository)) + { + result.Add(new ModGenericModel(modKey, "There's no mod repository matching this namespaced mod ID.")); + continue; + } + + // fetch mod info + result.Add(await repository.GetModInfoAsync(modID)); } + + return result.ToArray(); + } + + + /********* + ** Private methods + *********/ + /// Parse a namespaced mod ID. + /// The raw mod ID to parse. + /// The parsed vendor key. + /// The parsed mod ID. + /// Returns whether the value could be parsed. + private bool TryParseModKey(string raw, out string vendorKey, out string modID) + { + // split parts + string[] parts = raw?.Split(':'); + if (parts == null || parts.Length != 2) + { + vendorKey = null; + modID = null; + return false; + } + + // parse + vendorKey = parts[0]; + modID = parts[1]; + return true; } } } diff --git a/src/StardewModdingAPI.Web/Framework/IModRepository.cs b/src/StardewModdingAPI.Web/Framework/ModRepositories/IModRepository.cs similarity index 60% rename from src/StardewModdingAPI.Web/Framework/IModRepository.cs rename to src/StardewModdingAPI.Web/Framework/ModRepositories/IModRepository.cs index ebf9850f..43bad4e9 100644 --- a/src/StardewModdingAPI.Web/Framework/IModRepository.cs +++ b/src/StardewModdingAPI.Web/Framework/ModRepositories/IModRepository.cs @@ -2,16 +2,23 @@ using System; using System.Threading.Tasks; using StardewModdingAPI.Web.Models; -namespace StardewModdingAPI.Web.Framework +namespace StardewModdingAPI.Web.Framework.ModRepositories { /// A repository which provides mod metadata. internal interface IModRepository : IDisposable { + /********* + ** Accessors + *********/ + /// The unique key for this vendor. + string VendorKey { get; } + + /********* ** Public methods *********/ /// Get metadata about a mod in the repository. /// The mod ID in this repository. - Task GetModInfoAsync(int id); + Task GetModInfoAsync(string id); } } diff --git a/src/StardewModdingAPI.Web/Framework/NexusModsClient.cs b/src/StardewModdingAPI.Web/Framework/ModRepositories/NexusRepository.cs similarity index 75% rename from src/StardewModdingAPI.Web/Framework/NexusModsClient.cs rename to src/StardewModdingAPI.Web/Framework/ModRepositories/NexusRepository.cs index 8f010d56..37f309da 100644 --- a/src/StardewModdingAPI.Web/Framework/NexusModsClient.cs +++ b/src/StardewModdingAPI.Web/Framework/ModRepositories/NexusRepository.cs @@ -4,10 +4,10 @@ using Newtonsoft.Json; using Pathoschild.Http.Client; using StardewModdingAPI.Web.Models; -namespace StardewModdingAPI.Web.Framework +namespace StardewModdingAPI.Web.Framework.ModRepositories { /// An HTTP client for fetching mod metadata from Nexus Mods. - internal class NexusModsClient : IModRepository + internal class NexusRepository : IModRepository { /********* ** Properties @@ -15,11 +15,19 @@ namespace StardewModdingAPI.Web.Framework /// The underlying HTTP client. private readonly IClient Client; + + /********* + ** Accessors + *********/ + /// The unique key for this vendor. + public string VendorKey { get; } = "Nexus"; + + /********* ** Public methods *********/ /// Construct an instance. - public NexusModsClient() + public NexusRepository() { this.Client = new FluentClient("http://www.nexusmods.com/stardewvalley") .SetUserAgent("Nexus Client v0.63.15"); @@ -27,18 +35,18 @@ namespace StardewModdingAPI.Web.Framework /// Get metadata about a mod in the repository. /// The mod ID in this repository. - public async Task GetModInfoAsync(int id) + public async Task GetModInfoAsync(string id) { try { NexusResponseModel response = await this.Client .GetAsync($"mods/{id}") .As(); - return new ModGenericModel("Nexus", id, response.Name, response.Version, response.Url); + return new ModGenericModel($"{this.VendorKey}:{id}", response.Name, response.Version, response.Url); } - catch (Exception) + catch (Exception ex) { - return new ModGenericModel("Nexus", id); + return new ModGenericModel($"{this.VendorKey}:{id}", ex.ToString()); } } @@ -58,9 +66,6 @@ namespace StardewModdingAPI.Web.Framework /********* ** Accessors *********/ - /// The unique mod ID. - public int ID { get; set; } - /// The mod name. public string Name { get; set; } diff --git a/src/StardewModdingAPI.Web/Models/ModGenericModel.cs b/src/StardewModdingAPI.Web/Models/ModGenericModel.cs index dc36c7f4..88a6e4bd 100644 --- a/src/StardewModdingAPI.Web/Models/ModGenericModel.cs +++ b/src/StardewModdingAPI.Web/Models/ModGenericModel.cs @@ -1,3 +1,5 @@ +using Newtonsoft.Json; + namespace StardewModdingAPI.Web.Models { /// Generic metadata about a mod. @@ -6,53 +8,48 @@ namespace StardewModdingAPI.Web.Models /********* ** Accessors *********/ - /// The unique mod ID. - public int ID { get; } + /// The namespaced mod key. + public string ModKey { get; } /// The mod name. public string Name { get; } - /// The mod's vendor ID. - public string Vendor { get; } - /// The mod's semantic version number. public string Version { get; } /// The mod's web URL. public string Url { get; } - /// Whether the mod is valid. - public bool Valid { get; } + /// The error message indicating why the mod is invalid (if applicable). + public string Error { get; } /********* ** Public methods *********/ /// Construct a valid instance. - /// The mod's vendor ID. - /// The unique mod ID. + /// The namespaced mod key. /// The mod name. /// The mod's semantic version number. /// The mod's web URL. - /// Whether the mod is valid. - public ModGenericModel(string vendor, int id, string name, string version, string url, bool valid = true) + /// The error message indicating why the mod is invalid (if applicable). + [JsonConstructor] + public ModGenericModel(string modKey, string name, string version, string url, string error = null) { - this.Vendor = vendor; - this.ID = id; + this.ModKey = modKey; this.Name = name; this.Version = version; this.Url = url; - this.Valid = valid; + this.Error = error; // mainly initialised here for the JSON deserialiser } /// Construct an valid instance. - /// The mod's vendor ID. - /// The unique mod ID. - public ModGenericModel(string vendor, int id) + /// The namespaced mod key. + /// The error message indicating why the mod is invalid. + public ModGenericModel(string modKey, string error) { - this.Vendor = vendor; - this.ID = id; - this.Valid = false; + this.ModKey = modKey; + this.Error = error; } } } diff --git a/src/StardewModdingAPI.Web/Models/ModSearchModel.cs b/src/StardewModdingAPI.Web/Models/ModSearchModel.cs index eb9ac920..852ea439 100644 --- a/src/StardewModdingAPI.Web/Models/ModSearchModel.cs +++ b/src/StardewModdingAPI.Web/Models/ModSearchModel.cs @@ -1,9 +1,9 @@ namespace StardewModdingAPI.Web.Models { - /// The search criteria for a mod. + /// The mod update search criteria. public class ModSearchModel { - /// The Nexus Mods ID (if any). - public int? NexusID { get; set; } + /// The namespaced mod keys to search. + public string[] ModKeys { get; set; } } } From 9c072333d161d2510ee884d71dc9a714bbf86033 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 22 Sep 2017 00:58:25 -0400 Subject: [PATCH 069/186] rename mods endpoint & model (#336) --- .../{CheckController.cs => ModsController.cs} | 12 ++++++------ .../Framework/ModRepositories/IModRepository.cs | 2 +- .../Framework/ModRepositories/NexusRepository.cs | 6 +++--- .../Models/{ModGenericModel.cs => ModInfoModel.cs} | 6 +++--- 4 files changed, 13 insertions(+), 13 deletions(-) rename src/StardewModdingAPI.Web/Controllers/{CheckController.cs => ModsController.cs} (82%) rename src/StardewModdingAPI.Web/Models/{ModGenericModel.cs => ModInfoModel.cs} (89%) diff --git a/src/StardewModdingAPI.Web/Controllers/CheckController.cs b/src/StardewModdingAPI.Web/Controllers/ModsController.cs similarity index 82% rename from src/StardewModdingAPI.Web/Controllers/CheckController.cs rename to src/StardewModdingAPI.Web/Controllers/ModsController.cs index 2a346cf3..bbf1744f 100644 --- a/src/StardewModdingAPI.Web/Controllers/CheckController.cs +++ b/src/StardewModdingAPI.Web/Controllers/ModsController.cs @@ -10,8 +10,7 @@ namespace StardewModdingAPI.Web.Controllers { /// Provides an API to perform mod update checks. [Produces("application/json")] - [Route("api/check")] - public class CheckController : Controller + public class ModsController : Controller { /********* ** Properties @@ -31,23 +30,24 @@ namespace StardewModdingAPI.Web.Controllers /// Fetch version metadata for the given mods. /// The mod update search criteria. [HttpPost] - public async Task Post([FromBody] ModSearchModel search) + [Route("mods")] + public async Task Post([FromBody] ModSearchModel search) { - IList result = new List(); + IList result = new List(); foreach (string modKey in search.ModKeys) { // parse mod key if (!this.TryParseModKey(modKey, out string vendorKey, out string modID)) { - result.Add(new ModGenericModel(modKey, "The mod key isn't in a valid format. It should contain the mod repository key and mod ID like 'Nexus:541'.")); + result.Add(new ModInfoModel(modKey, "The mod key isn't in a valid format. It should contain the mod repository key and mod ID like 'Nexus:541'.")); continue; } // get matching repository if (!this.Repositories.TryGetValue(vendorKey, out IModRepository repository)) { - result.Add(new ModGenericModel(modKey, "There's no mod repository matching this namespaced mod ID.")); + result.Add(new ModInfoModel(modKey, "There's no mod repository matching this namespaced mod ID.")); continue; } diff --git a/src/StardewModdingAPI.Web/Framework/ModRepositories/IModRepository.cs b/src/StardewModdingAPI.Web/Framework/ModRepositories/IModRepository.cs index 43bad4e9..7fd735cd 100644 --- a/src/StardewModdingAPI.Web/Framework/ModRepositories/IModRepository.cs +++ b/src/StardewModdingAPI.Web/Framework/ModRepositories/IModRepository.cs @@ -19,6 +19,6 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories *********/ /// Get metadata about a mod in the repository. /// The mod ID in this repository. - Task GetModInfoAsync(string id); + Task GetModInfoAsync(string id); } } diff --git a/src/StardewModdingAPI.Web/Framework/ModRepositories/NexusRepository.cs b/src/StardewModdingAPI.Web/Framework/ModRepositories/NexusRepository.cs index 37f309da..74eef2ef 100644 --- a/src/StardewModdingAPI.Web/Framework/ModRepositories/NexusRepository.cs +++ b/src/StardewModdingAPI.Web/Framework/ModRepositories/NexusRepository.cs @@ -35,18 +35,18 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories /// Get metadata about a mod in the repository. /// The mod ID in this repository. - public async Task GetModInfoAsync(string id) + public async Task GetModInfoAsync(string id) { try { NexusResponseModel response = await this.Client .GetAsync($"mods/{id}") .As(); - return new ModGenericModel($"{this.VendorKey}:{id}", response.Name, response.Version, response.Url); + return new ModInfoModel($"{this.VendorKey}:{id}", response.Name, response.Version, response.Url); } catch (Exception ex) { - return new ModGenericModel($"{this.VendorKey}:{id}", ex.ToString()); + return new ModInfoModel($"{this.VendorKey}:{id}", ex.ToString()); } } diff --git a/src/StardewModdingAPI.Web/Models/ModGenericModel.cs b/src/StardewModdingAPI.Web/Models/ModInfoModel.cs similarity index 89% rename from src/StardewModdingAPI.Web/Models/ModGenericModel.cs rename to src/StardewModdingAPI.Web/Models/ModInfoModel.cs index 88a6e4bd..723d6c73 100644 --- a/src/StardewModdingAPI.Web/Models/ModGenericModel.cs +++ b/src/StardewModdingAPI.Web/Models/ModInfoModel.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace StardewModdingAPI.Web.Models { /// Generic metadata about a mod. - public class ModGenericModel + public class ModInfoModel { /********* ** Accessors @@ -34,7 +34,7 @@ namespace StardewModdingAPI.Web.Models /// The mod's web URL. /// The error message indicating why the mod is invalid (if applicable). [JsonConstructor] - public ModGenericModel(string modKey, string name, string version, string url, string error = null) + public ModInfoModel(string modKey, string name, string version, string url, string error = null) { this.ModKey = modKey; this.Name = name; @@ -46,7 +46,7 @@ namespace StardewModdingAPI.Web.Models /// Construct an valid instance. /// The namespaced mod key. /// The error message indicating why the mod is invalid. - public ModGenericModel(string modKey, string error) + public ModInfoModel(string modKey, string error) { this.ModKey = modKey; this.Error = error; From ef60b8d32abf7c8613749766155d80139e33b9d1 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 22 Sep 2017 01:23:27 -0400 Subject: [PATCH 070/186] add version number to route for future use (#336) --- src/StardewModdingAPI.Web/Controllers/ModsController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StardewModdingAPI.Web/Controllers/ModsController.cs b/src/StardewModdingAPI.Web/Controllers/ModsController.cs index bbf1744f..c7b2aba7 100644 --- a/src/StardewModdingAPI.Web/Controllers/ModsController.cs +++ b/src/StardewModdingAPI.Web/Controllers/ModsController.cs @@ -9,6 +9,7 @@ using StardewModdingAPI.Web.Models; namespace StardewModdingAPI.Web.Controllers { /// Provides an API to perform mod update checks. + [Route("v1.0/mods")] [Produces("application/json")] public class ModsController : Controller { @@ -30,7 +31,6 @@ namespace StardewModdingAPI.Web.Controllers /// Fetch version metadata for the given mods. /// The mod update search criteria. [HttpPost] - [Route("mods")] public async Task Post([FromBody] ModSearchModel search) { IList result = new List(); From 86e55596786f8d65854a75632512750b6e09faae Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 22 Sep 2017 01:57:18 -0400 Subject: [PATCH 071/186] switch mod update endpoint to GET with comma-delimited mod keys (#336) --- .../Controllers/ModsController.cs | 8 +-- .../Framework/CommaDelimitedModelBinder.cs | 58 +++++++++++++++++++ .../CommaDelimitedModelBinderProvider.cs | 27 +++++++++ .../Models/ModSearchModel.cs | 9 --- src/StardewModdingAPI.Web/Startup.cs | 13 ++++- 5 files changed, 100 insertions(+), 15 deletions(-) create mode 100644 src/StardewModdingAPI.Web/Framework/CommaDelimitedModelBinder.cs create mode 100644 src/StardewModdingAPI.Web/Framework/CommaDelimitedModelBinderProvider.cs delete mode 100644 src/StardewModdingAPI.Web/Models/ModSearchModel.cs diff --git a/src/StardewModdingAPI.Web/Controllers/ModsController.cs b/src/StardewModdingAPI.Web/Controllers/ModsController.cs index c7b2aba7..d3b49445 100644 --- a/src/StardewModdingAPI.Web/Controllers/ModsController.cs +++ b/src/StardewModdingAPI.Web/Controllers/ModsController.cs @@ -29,13 +29,13 @@ namespace StardewModdingAPI.Web.Controllers ** Public methods *********/ /// Fetch version metadata for the given mods. - /// The mod update search criteria. - [HttpPost] - public async Task Post([FromBody] ModSearchModel search) + /// The namespaced mod keys to search. + [HttpGet] + public async Task Post(IEnumerable modKeys) { IList result = new List(); - foreach (string modKey in search.ModKeys) + foreach (string modKey in modKeys) { // parse mod key if (!this.TryParseModKey(modKey, out string vendorKey, out string modID)) diff --git a/src/StardewModdingAPI.Web/Framework/CommaDelimitedModelBinder.cs b/src/StardewModdingAPI.Web/Framework/CommaDelimitedModelBinder.cs new file mode 100644 index 00000000..119b18e6 --- /dev/null +++ b/src/StardewModdingAPI.Web/Framework/CommaDelimitedModelBinder.cs @@ -0,0 +1,58 @@ +using System; +using System.ComponentModel; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace StardewModdingAPI.Web.Framework +{ + /// Maps comma-delimited values to an parameter. + /// Derived from . + public class CommaDelimitedModelBinder : IModelBinder + { + /********* + ** Public methods + *********/ + /// Attempts to bind a model. + /// The model binding context. + public Task BindModelAsync(ModelBindingContext bindingContext) + { + // validate + if (bindingContext == null) + throw new ArgumentNullException(nameof(bindingContext)); + + // extract values + string modelName = bindingContext.ModelName; + ValueProviderResult valueProviderResult = bindingContext.ValueProvider.GetValue(modelName); + string[] values = valueProviderResult + .ToString() + .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + Type elementType = bindingContext.ModelType.GetTypeInfo().GenericTypeArguments[0]; + if (values.Length == 0) + { + bindingContext.Result = ModelBindingResult.Success(Array.CreateInstance(elementType, 0)); + return Task.CompletedTask; + } + + // map values + TypeConverter converter = TypeDescriptor.GetConverter(elementType); + Array typedArray = Array.CreateInstance(elementType, values.Length); + try + { + for (int i = 0; i < values.Length; ++i) + { + string value = values[i]; + object convertedValue = converter.ConvertFromString(value); + typedArray.SetValue(convertedValue, i); + } + } + catch (Exception exception) + { + bindingContext.ModelState.TryAddModelError(modelName, exception, bindingContext.ModelMetadata); + } + + bindingContext.Result = ModelBindingResult.Success(typedArray); + return Task.CompletedTask; + } + } +} diff --git a/src/StardewModdingAPI.Web/Framework/CommaDelimitedModelBinderProvider.cs b/src/StardewModdingAPI.Web/Framework/CommaDelimitedModelBinderProvider.cs new file mode 100644 index 00000000..1b3f0073 --- /dev/null +++ b/src/StardewModdingAPI.Web/Framework/CommaDelimitedModelBinderProvider.cs @@ -0,0 +1,27 @@ +using System; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace StardewModdingAPI.Web.Framework +{ + /// Provides comma-delimited model binds for mapping parameters. + /// Derived from . + public class CommaDelimitedModelBinderProvider : IModelBinderProvider + { + /********* + ** Public methods + *********/ + /// Creates a model binder based on the given context. + /// The model binding context. + public IModelBinder GetBinder(ModelBinderProviderContext context) + { + // validate + if (context == null) + throw new ArgumentNullException(nameof(context)); + + // get model binder + return context.Metadata.IsEnumerableType && !context.Metadata.ElementMetadata.IsComplexType + ? new CommaDelimitedModelBinder() + : null; + } + } +} diff --git a/src/StardewModdingAPI.Web/Models/ModSearchModel.cs b/src/StardewModdingAPI.Web/Models/ModSearchModel.cs deleted file mode 100644 index 852ea439..00000000 --- a/src/StardewModdingAPI.Web/Models/ModSearchModel.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace StardewModdingAPI.Web.Models -{ - /// The mod update search criteria. - public class ModSearchModel - { - /// The namespaced mod keys to search. - public string[] ModKeys { get; set; } - } -} diff --git a/src/StardewModdingAPI.Web/Startup.cs b/src/StardewModdingAPI.Web/Startup.cs index c7a5e8fe..c1f03b34 100644 --- a/src/StardewModdingAPI.Web/Startup.cs +++ b/src/StardewModdingAPI.Web/Startup.cs @@ -1,8 +1,11 @@ -using Microsoft.AspNetCore.Builder; +using System.Linq; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.ModelBinding.Binders; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using StardewModdingAPI.Web.Framework; namespace StardewModdingAPI.Web { @@ -35,7 +38,13 @@ namespace StardewModdingAPI.Web /// The service injection container. public void ConfigureServices(IServiceCollection services) { - services.AddMvc(); + services + .AddMvc(options => + { + // add support for comma-delimited parameters + ArrayModelBinderProvider arrayModelBinderProvider = options.ModelBinderProviders.OfType().First(); + options.ModelBinderProviders.Insert(options.ModelBinderProviders.IndexOf(arrayModelBinderProvider), new CommaDelimitedModelBinderProvider()); + }); } /// The method called by the runtime to configure the HTTP request pipeline. From 7d703c9c5ca76a99fc3d430fa2b3fd22301a5ffb Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 22 Sep 2017 02:00:15 -0400 Subject: [PATCH 072/186] handle invalid Nexus mod IDs (#336) --- .../Framework/ModRepositories/NexusRepository.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/StardewModdingAPI.Web/Framework/ModRepositories/NexusRepository.cs b/src/StardewModdingAPI.Web/Framework/ModRepositories/NexusRepository.cs index 74eef2ef..7e3ce4b6 100644 --- a/src/StardewModdingAPI.Web/Framework/ModRepositories/NexusRepository.cs +++ b/src/StardewModdingAPI.Web/Framework/ModRepositories/NexusRepository.cs @@ -42,7 +42,10 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories NexusResponseModel response = await this.Client .GetAsync($"mods/{id}") .As(); - return new ModInfoModel($"{this.VendorKey}:{id}", response.Name, response.Version, response.Url); + + return response != null + ? new ModInfoModel($"{this.VendorKey}:{id}", response.Name, response.Version, response.Url) + : new ModInfoModel($"{this.VendorKey}:{id}", "Found no mod with this ID."); } catch (Exception ex) { From 399b98b36b6111d364702b117fff3c5f21b8783a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 22 Sep 2017 02:06:54 -0400 Subject: [PATCH 073/186] suppress null properties in JSON responses (#336) --- src/StardewModdingAPI.Web/Startup.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/StardewModdingAPI.Web/Startup.cs b/src/StardewModdingAPI.Web/Startup.cs index c1f03b34..2d9a95f1 100644 --- a/src/StardewModdingAPI.Web/Startup.cs +++ b/src/StardewModdingAPI.Web/Startup.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding.Binders; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Newtonsoft.Json; using StardewModdingAPI.Web.Framework; namespace StardewModdingAPI.Web @@ -44,6 +45,11 @@ namespace StardewModdingAPI.Web // add support for comma-delimited parameters ArrayModelBinderProvider arrayModelBinderProvider = options.ModelBinderProviders.OfType().First(); options.ModelBinderProviders.Insert(options.ModelBinderProviders.IndexOf(arrayModelBinderProvider), new CommaDelimitedModelBinderProvider()); + }) + .AddJsonOptions(options => + { + // suppress null values in JSON responses + options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; }); } From 86d4827df211cc28549acb88ee7cb08d6cc4d4aa Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 22 Sep 2017 03:01:40 -0400 Subject: [PATCH 074/186] simplify input & output format (#336) --- .../Controllers/ModsController.cs | 22 ++++--- .../Framework/CommaDelimitedModelBinder.cs | 58 ------------------- .../CommaDelimitedModelBinderProvider.cs | 27 --------- .../ModRepositories/NexusRepository.cs | 6 +- .../Models/ModInfoModel.cs | 11 +--- .../Models/ModSearchModel.cs | 9 +++ src/StardewModdingAPI.Web/Startup.cs | 16 +---- 7 files changed, 30 insertions(+), 119 deletions(-) delete mode 100644 src/StardewModdingAPI.Web/Framework/CommaDelimitedModelBinder.cs delete mode 100644 src/StardewModdingAPI.Web/Framework/CommaDelimitedModelBinderProvider.cs create mode 100644 src/StardewModdingAPI.Web/Models/ModSearchModel.cs diff --git a/src/StardewModdingAPI.Web/Controllers/ModsController.cs b/src/StardewModdingAPI.Web/Controllers/ModsController.cs index d3b49445..876f5248 100644 --- a/src/StardewModdingAPI.Web/Controllers/ModsController.cs +++ b/src/StardewModdingAPI.Web/Controllers/ModsController.cs @@ -29,33 +29,39 @@ namespace StardewModdingAPI.Web.Controllers ** Public methods *********/ /// Fetch version metadata for the given mods. - /// The namespaced mod keys to search. - [HttpGet] - public async Task Post(IEnumerable modKeys) + /// The search options. + [HttpPost] + public async Task> Post([FromBody] ModSearchModel search) { - IList result = new List(); + // sort & filter keys + string[] modKeys = (search.ModKeys ?? new string[0]) + .Distinct(StringComparer.CurrentCultureIgnoreCase) + .OrderBy(p => p, StringComparer.CurrentCultureIgnoreCase) + .ToArray(); + // fetch mod info + IDictionary result = new Dictionary(StringComparer.CurrentCultureIgnoreCase); foreach (string modKey in modKeys) { // parse mod key if (!this.TryParseModKey(modKey, out string vendorKey, out string modID)) { - result.Add(new ModInfoModel(modKey, "The mod key isn't in a valid format. It should contain the mod repository key and mod ID like 'Nexus:541'.")); + result[modKey] = new ModInfoModel("The mod key isn't in a valid format. It should contain the mod repository key and mod ID like 'Nexus:541'."); continue; } // get matching repository if (!this.Repositories.TryGetValue(vendorKey, out IModRepository repository)) { - result.Add(new ModInfoModel(modKey, "There's no mod repository matching this namespaced mod ID.")); + result[modKey] = new ModInfoModel("There's no mod repository matching this namespaced mod ID."); continue; } // fetch mod info - result.Add(await repository.GetModInfoAsync(modID)); + result[modKey] = await repository.GetModInfoAsync(modID); } - return result.ToArray(); + return result; } diff --git a/src/StardewModdingAPI.Web/Framework/CommaDelimitedModelBinder.cs b/src/StardewModdingAPI.Web/Framework/CommaDelimitedModelBinder.cs deleted file mode 100644 index 119b18e6..00000000 --- a/src/StardewModdingAPI.Web/Framework/CommaDelimitedModelBinder.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using System.ComponentModel; -using System.Reflection; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc.ModelBinding; - -namespace StardewModdingAPI.Web.Framework -{ - /// Maps comma-delimited values to an parameter. - /// Derived from . - public class CommaDelimitedModelBinder : IModelBinder - { - /********* - ** Public methods - *********/ - /// Attempts to bind a model. - /// The model binding context. - public Task BindModelAsync(ModelBindingContext bindingContext) - { - // validate - if (bindingContext == null) - throw new ArgumentNullException(nameof(bindingContext)); - - // extract values - string modelName = bindingContext.ModelName; - ValueProviderResult valueProviderResult = bindingContext.ValueProvider.GetValue(modelName); - string[] values = valueProviderResult - .ToString() - .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - Type elementType = bindingContext.ModelType.GetTypeInfo().GenericTypeArguments[0]; - if (values.Length == 0) - { - bindingContext.Result = ModelBindingResult.Success(Array.CreateInstance(elementType, 0)); - return Task.CompletedTask; - } - - // map values - TypeConverter converter = TypeDescriptor.GetConverter(elementType); - Array typedArray = Array.CreateInstance(elementType, values.Length); - try - { - for (int i = 0; i < values.Length; ++i) - { - string value = values[i]; - object convertedValue = converter.ConvertFromString(value); - typedArray.SetValue(convertedValue, i); - } - } - catch (Exception exception) - { - bindingContext.ModelState.TryAddModelError(modelName, exception, bindingContext.ModelMetadata); - } - - bindingContext.Result = ModelBindingResult.Success(typedArray); - return Task.CompletedTask; - } - } -} diff --git a/src/StardewModdingAPI.Web/Framework/CommaDelimitedModelBinderProvider.cs b/src/StardewModdingAPI.Web/Framework/CommaDelimitedModelBinderProvider.cs deleted file mode 100644 index 1b3f0073..00000000 --- a/src/StardewModdingAPI.Web/Framework/CommaDelimitedModelBinderProvider.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using Microsoft.AspNetCore.Mvc.ModelBinding; - -namespace StardewModdingAPI.Web.Framework -{ - /// Provides comma-delimited model binds for mapping parameters. - /// Derived from . - public class CommaDelimitedModelBinderProvider : IModelBinderProvider - { - /********* - ** Public methods - *********/ - /// Creates a model binder based on the given context. - /// The model binding context. - public IModelBinder GetBinder(ModelBinderProviderContext context) - { - // validate - if (context == null) - throw new ArgumentNullException(nameof(context)); - - // get model binder - return context.Metadata.IsEnumerableType && !context.Metadata.ElementMetadata.IsComplexType - ? new CommaDelimitedModelBinder() - : null; - } - } -} diff --git a/src/StardewModdingAPI.Web/Framework/ModRepositories/NexusRepository.cs b/src/StardewModdingAPI.Web/Framework/ModRepositories/NexusRepository.cs index 7e3ce4b6..02c2939a 100644 --- a/src/StardewModdingAPI.Web/Framework/ModRepositories/NexusRepository.cs +++ b/src/StardewModdingAPI.Web/Framework/ModRepositories/NexusRepository.cs @@ -44,12 +44,12 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories .As(); return response != null - ? new ModInfoModel($"{this.VendorKey}:{id}", response.Name, response.Version, response.Url) - : new ModInfoModel($"{this.VendorKey}:{id}", "Found no mod with this ID."); + ? new ModInfoModel(response.Name, response.Version, response.Url) + : new ModInfoModel("Found no mod with this ID."); } catch (Exception ex) { - return new ModInfoModel($"{this.VendorKey}:{id}", ex.ToString()); + return new ModInfoModel(ex.ToString()); } } diff --git a/src/StardewModdingAPI.Web/Models/ModInfoModel.cs b/src/StardewModdingAPI.Web/Models/ModInfoModel.cs index 723d6c73..180420cd 100644 --- a/src/StardewModdingAPI.Web/Models/ModInfoModel.cs +++ b/src/StardewModdingAPI.Web/Models/ModInfoModel.cs @@ -8,9 +8,6 @@ namespace StardewModdingAPI.Web.Models /********* ** Accessors *********/ - /// The namespaced mod key. - public string ModKey { get; } - /// The mod name. public string Name { get; } @@ -28,15 +25,13 @@ namespace StardewModdingAPI.Web.Models ** Public methods *********/ /// Construct a valid instance. - /// The namespaced mod key. /// The mod name. /// The mod's semantic version number. /// The mod's web URL. /// The error message indicating why the mod is invalid (if applicable). [JsonConstructor] - public ModInfoModel(string modKey, string name, string version, string url, string error = null) + public ModInfoModel(string name, string version, string url, string error = null) { - this.ModKey = modKey; this.Name = name; this.Version = version; this.Url = url; @@ -44,11 +39,9 @@ namespace StardewModdingAPI.Web.Models } /// Construct an valid instance. - /// The namespaced mod key. /// The error message indicating why the mod is invalid. - public ModInfoModel(string modKey, string error) + public ModInfoModel(string error) { - this.ModKey = modKey; this.Error = error; } } diff --git a/src/StardewModdingAPI.Web/Models/ModSearchModel.cs b/src/StardewModdingAPI.Web/Models/ModSearchModel.cs new file mode 100644 index 00000000..b5bd12fb --- /dev/null +++ b/src/StardewModdingAPI.Web/Models/ModSearchModel.cs @@ -0,0 +1,9 @@ +namespace StardewModdingAPI.Web.Models +{ + /// Metadata for mods to look up. + public class ModSearchModel + { + /// The namespaced mod keys to search. + public string[] ModKeys { get; set; } + } +} diff --git a/src/StardewModdingAPI.Web/Startup.cs b/src/StardewModdingAPI.Web/Startup.cs index 2d9a95f1..abae06ec 100644 --- a/src/StardewModdingAPI.Web/Startup.cs +++ b/src/StardewModdingAPI.Web/Startup.cs @@ -1,12 +1,9 @@ -using System.Linq; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Mvc.ModelBinding.Binders; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Newtonsoft.Json; -using StardewModdingAPI.Web.Framework; namespace StardewModdingAPI.Web { @@ -40,17 +37,8 @@ namespace StardewModdingAPI.Web public void ConfigureServices(IServiceCollection services) { services - .AddMvc(options => - { - // add support for comma-delimited parameters - ArrayModelBinderProvider arrayModelBinderProvider = options.ModelBinderProviders.OfType().First(); - options.ModelBinderProviders.Insert(options.ModelBinderProviders.IndexOf(arrayModelBinderProvider), new CommaDelimitedModelBinderProvider()); - }) - .AddJsonOptions(options => - { - // suppress null values in JSON responses - options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; - }); + .AddMvc() + .AddJsonOptions(options => options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore); // suppress null values in JSON responses } /// The method called by the runtime to configure the HTTP request pipeline. From 5619890abf866e608e667bc6cd81b5aba9e3a350 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 22 Sep 2017 18:19:42 -0400 Subject: [PATCH 075/186] indent JSON responses (#336) --- src/StardewModdingAPI.Web/Startup.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/StardewModdingAPI.Web/Startup.cs b/src/StardewModdingAPI.Web/Startup.cs index abae06ec..b35d072b 100644 --- a/src/StardewModdingAPI.Web/Startup.cs +++ b/src/StardewModdingAPI.Web/Startup.cs @@ -38,7 +38,11 @@ namespace StardewModdingAPI.Web { services .AddMvc() - .AddJsonOptions(options => options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore); // suppress null values in JSON responses + .AddJsonOptions(options => + { + options.SerializerSettings.Formatting = Formatting.Indented; + options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; + }); } /// The method called by the runtime to configure the HTTP request pipeline. From 89ca5952c51128c23c40b6264aa125ad7e5e2305 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 22 Sep 2017 19:24:31 -0400 Subject: [PATCH 076/186] update web API to .NET Core 2.0 so we can use more packages, update all packages (#336) --- .../StardewModdingAPI.Tests.csproj | 4 ++-- src/StardewModdingAPI.Tests/packages.config | 4 ++-- .../StardewModdingAPI.Web.csproj | 22 +++++++++---------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/StardewModdingAPI.Tests/StardewModdingAPI.Tests.csproj b/src/StardewModdingAPI.Tests/StardewModdingAPI.Tests.csproj index f3dbcdd4..41525bcb 100644 --- a/src/StardewModdingAPI.Tests/StardewModdingAPI.Tests.csproj +++ b/src/StardewModdingAPI.Tests/StardewModdingAPI.Tests.csproj @@ -39,8 +39,8 @@ ..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll - - ..\packages\NUnit.3.7.1\lib\net45\nunit.framework.dll + + ..\packages\NUnit.3.8.1\lib\net45\nunit.framework.dll diff --git a/src/StardewModdingAPI.Tests/packages.config b/src/StardewModdingAPI.Tests/packages.config index 6f04e625..5fdfebdb 100644 --- a/src/StardewModdingAPI.Tests/packages.config +++ b/src/StardewModdingAPI.Tests/packages.config @@ -3,5 +3,5 @@ - - \ No newline at end of file + + diff --git a/src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj b/src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj index eee0f3f3..49f8ca23 100644 --- a/src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj +++ b/src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj @@ -1,7 +1,7 @@  - netcoreapp1.1 + netcoreapp2.0 portable-net45+win8 @@ -9,16 +9,16 @@ - - - - - - - - - - + + + + + + + + + + From 24afbad3a909f70f3022fe57d06f6188a5b760bb Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 22 Sep 2017 19:55:57 -0400 Subject: [PATCH 077/186] remove unneeded project settings (#336) --- src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj b/src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj index 49f8ca23..01ed0c39 100644 --- a/src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj +++ b/src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj @@ -2,12 +2,8 @@ netcoreapp2.0 - portable-net45+win8 - - - From 67ce5fcc459ac2e2eface6593592903dd1b68290 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 22 Sep 2017 20:09:06 -0400 Subject: [PATCH 078/186] remove unneeded dependencies (#336) --- src/StardewModdingAPI.Web/Program.cs | 3 +-- src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj | 6 ------ 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/StardewModdingAPI.Web/Program.cs b/src/StardewModdingAPI.Web/Program.cs index 5e258acc..eeecb791 100644 --- a/src/StardewModdingAPI.Web/Program.cs +++ b/src/StardewModdingAPI.Web/Program.cs @@ -1,4 +1,4 @@ -using System.IO; +using System.IO; using Microsoft.AspNetCore.Hosting; namespace StardewModdingAPI.Web @@ -19,7 +19,6 @@ namespace StardewModdingAPI.Web .UseContentRoot(Directory.GetCurrentDirectory()) .UseIISIntegration() .UseStartup() - .UseApplicationInsights() .Build() .Run(); } diff --git a/src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj b/src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj index 01ed0c39..7dff3fb6 100644 --- a/src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj +++ b/src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj @@ -5,16 +5,10 @@ - - - - - - From f8566067e0874428c53ffdb6cd12d3f91ad4c51c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 22 Sep 2017 20:09:22 -0400 Subject: [PATCH 079/186] update launch URL (#336) --- src/StardewModdingAPI.Web/Properties/launchSettings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/StardewModdingAPI.Web/Properties/launchSettings.json b/src/StardewModdingAPI.Web/Properties/launchSettings.json index c15134dc..3acee14d 100644 --- a/src/StardewModdingAPI.Web/Properties/launchSettings.json +++ b/src/StardewModdingAPI.Web/Properties/launchSettings.json @@ -1,4 +1,4 @@ -{ +{ "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, @@ -11,7 +11,7 @@ "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, - "launchUrl": "api/check", + "launchUrl": "api/v1.0/mods", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } @@ -19,7 +19,7 @@ "Dewdrop": { "commandName": "Project", "launchBrowser": true, - "launchUrl": "api/check", + "launchUrl": "api/v1.0/mods", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, From 2406d4b0a6fd8b2d439438687b0101a6f8934ca5 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 22 Sep 2017 20:19:12 -0400 Subject: [PATCH 080/186] add subdomain rewrite rule (#336) --- .../Controllers/ModsController.cs | 2 +- .../Framework/RewriteSubdomainRule.cs | 30 +++++++++++++++++++ .../StardewModdingAPI.Web.csproj | 1 + src/StardewModdingAPI.Web/Startup.cs | 6 +++- 4 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 src/StardewModdingAPI.Web/Framework/RewriteSubdomainRule.cs diff --git a/src/StardewModdingAPI.Web/Controllers/ModsController.cs b/src/StardewModdingAPI.Web/Controllers/ModsController.cs index 876f5248..7f7afbb9 100644 --- a/src/StardewModdingAPI.Web/Controllers/ModsController.cs +++ b/src/StardewModdingAPI.Web/Controllers/ModsController.cs @@ -9,7 +9,7 @@ using StardewModdingAPI.Web.Models; namespace StardewModdingAPI.Web.Controllers { /// Provides an API to perform mod update checks. - [Route("v1.0/mods")] + [Route("api/v1.0/mods")] [Produces("application/json")] public class ModsController : Controller { diff --git a/src/StardewModdingAPI.Web/Framework/RewriteSubdomainRule.cs b/src/StardewModdingAPI.Web/Framework/RewriteSubdomainRule.cs new file mode 100644 index 00000000..9b89cb65 --- /dev/null +++ b/src/StardewModdingAPI.Web/Framework/RewriteSubdomainRule.cs @@ -0,0 +1,30 @@ +using System; +using Microsoft.AspNetCore.Rewrite; + +namespace StardewModdingAPI.Web.Framework +{ + /// Rewrite requests to prepend the subdomain portion (if any) to the path. + /// Derived from . + public class RewriteSubdomainRule : IRule + { + /// Applies the rule. Implementations of ApplyRule should set the value for (defaults to RuleResult.ContinueRules). + /// The rewrite context. + public void ApplyRule(RewriteContext context) + { + context.Result = RuleResult.ContinueRules; + + // get host parts + string host = context.HttpContext.Request.Host.Host; + string[] parts = host.Split('.'); + + // validate + if (parts.Length < 2) + return; + if (parts.Length < 3 && !"localhost".Equals(parts[1], StringComparison.InvariantCultureIgnoreCase)) + return; + + // prepend to path + context.HttpContext.Request.Path = $"/{parts[0]}{context.HttpContext.Request.Path}"; + } + } +} diff --git a/src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj b/src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj index 7dff3fb6..2af7c3df 100644 --- a/src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj +++ b/src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj @@ -7,6 +7,7 @@ + diff --git a/src/StardewModdingAPI.Web/Startup.cs b/src/StardewModdingAPI.Web/Startup.cs index b35d072b..19dffb88 100644 --- a/src/StardewModdingAPI.Web/Startup.cs +++ b/src/StardewModdingAPI.Web/Startup.cs @@ -1,9 +1,11 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Rewrite; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Newtonsoft.Json; +using StardewModdingAPI.Web.Framework; namespace StardewModdingAPI.Web { @@ -53,7 +55,9 @@ namespace StardewModdingAPI.Web { loggerFactory.AddConsole(this.Configuration.GetSection("Logging")); loggerFactory.AddDebug(); - app.UseMvc(); + app + .UseRewriter(new RewriteOptions().Add(new RewriteSubdomainRule())) // convert subdomain.smapi.io => smapi.io/subdomain for routing + .UseMvc(); } } } From e4a2f555170b08263ae2d7e713808bd229b48947 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 22 Sep 2017 20:33:09 -0400 Subject: [PATCH 081/186] add GET endpoint for testing (#336) --- .../Controllers/ModsController.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/StardewModdingAPI.Web/Controllers/ModsController.cs b/src/StardewModdingAPI.Web/Controllers/ModsController.cs index 7f7afbb9..366201e3 100644 --- a/src/StardewModdingAPI.Web/Controllers/ModsController.cs +++ b/src/StardewModdingAPI.Web/Controllers/ModsController.cs @@ -28,6 +28,17 @@ namespace StardewModdingAPI.Web.Controllers /********* ** Public methods *********/ + /// Fetch version metadata for the given mods. + /// The namespaced mod keys to search as a comma-delimited array. + [HttpGet] + public async Task> GetAsync(string modKeys) + { + return await this.Post(new ModSearchModel + { + ModKeys = modKeys?.Split(',').Select(p => p.Trim()).ToArray() ?? new string[0] + }); + } + /// Fetch version metadata for the given mods. /// The search options. [HttpPost] From ba5cc149e265d3f14246db23b09c8feb5f9c0d3a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 22 Sep 2017 20:46:38 -0400 Subject: [PATCH 082/186] add in-memory cache for remote queries (#336) --- .../Controllers/ModsController.cs | 17 ++++++++++++++++- src/StardewModdingAPI.Web/Startup.cs | 1 + 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/StardewModdingAPI.Web/Controllers/ModsController.cs b/src/StardewModdingAPI.Web/Controllers/ModsController.cs index 366201e3..bd5ecef9 100644 --- a/src/StardewModdingAPI.Web/Controllers/ModsController.cs +++ b/src/StardewModdingAPI.Web/Controllers/ModsController.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Caching.Memory; using StardewModdingAPI.Web.Framework.ModRepositories; using StardewModdingAPI.Web.Models; @@ -24,10 +25,20 @@ namespace StardewModdingAPI.Web.Controllers } .ToDictionary(p => p.VendorKey, StringComparer.CurrentCultureIgnoreCase); + /// The cache in which to store mod metadata. + private readonly IMemoryCache Cache; + /********* ** Public methods *********/ + /// Construct an instance. + /// The cache in which to store mod metadata. + public ModsController(IMemoryCache cache) + { + this.Cache = cache; + } + /// Fetch version metadata for the given mods. /// The namespaced mod keys to search as a comma-delimited array. [HttpGet] @@ -69,7 +80,11 @@ namespace StardewModdingAPI.Web.Controllers } // fetch mod info - result[modKey] = await repository.GetModInfoAsync(modID); + result[modKey] = await this.Cache.GetOrCreateAsync($"{repository.VendorKey}:{modID}".ToLower(), async entry => + { + entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddHours(1); + return await repository.GetModInfoAsync(modID); + }); } return result; diff --git a/src/StardewModdingAPI.Web/Startup.cs b/src/StardewModdingAPI.Web/Startup.cs index 19dffb88..8aba6a5a 100644 --- a/src/StardewModdingAPI.Web/Startup.cs +++ b/src/StardewModdingAPI.Web/Startup.cs @@ -39,6 +39,7 @@ namespace StardewModdingAPI.Web public void ConfigureServices(IServiceCollection services) { services + .AddMemoryCache() .AddMvc() .AddJsonOptions(options => { From ecdbefffd9c0acbbecd8178d7d2ac285715b5e7f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 22 Sep 2017 21:49:05 -0400 Subject: [PATCH 083/186] move hardcoded values into config (#336) --- .../Controllers/ModsController.cs | 27 +++++++++++++------ .../ConfigModels/ModUpdateCheckConfig.cs | 21 +++++++++++++++ .../ModRepositories/NexusRepository.cs | 18 +++++++++---- src/StardewModdingAPI.Web/Startup.cs | 2 ++ src/StardewModdingAPI.Web/appsettings.json | 11 ++++++-- 5 files changed, 64 insertions(+), 15 deletions(-) create mode 100644 src/StardewModdingAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs diff --git a/src/StardewModdingAPI.Web/Controllers/ModsController.cs b/src/StardewModdingAPI.Web/Controllers/ModsController.cs index bd5ecef9..50c23b99 100644 --- a/src/StardewModdingAPI.Web/Controllers/ModsController.cs +++ b/src/StardewModdingAPI.Web/Controllers/ModsController.cs @@ -4,6 +4,8 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using StardewModdingAPI.Web.Framework.ConfigModels; using StardewModdingAPI.Web.Framework.ModRepositories; using StardewModdingAPI.Web.Models; @@ -18,25 +20,34 @@ namespace StardewModdingAPI.Web.Controllers ** Properties *********/ /// The mod repositories which provide mod metadata. - private readonly IDictionary Repositories = - new IModRepository[] - { - new NexusRepository() - } - .ToDictionary(p => p.VendorKey, StringComparer.CurrentCultureIgnoreCase); + private readonly IDictionary Repositories; /// The cache in which to store mod metadata. private readonly IMemoryCache Cache; + /// The number of minutes update checks should be cached before refetching them. + private readonly int CacheMinutes; + /********* ** Public methods *********/ /// Construct an instance. /// The cache in which to store mod metadata. - public ModsController(IMemoryCache cache) + /// The config settings for mod update checks. + public ModsController(IMemoryCache cache, IOptions configProvider) { + ModUpdateCheckConfig config = configProvider.Value; + this.Cache = cache; + this.CacheMinutes = config.CacheMinutes; + + this.Repositories = + new IModRepository[] + { + new NexusRepository(config.NexusKey, config.NexusUserAgent, config.NexusBaseUrl, config.NexusModUrlFormat) + } + .ToDictionary(p => p.VendorKey, StringComparer.CurrentCultureIgnoreCase); } /// Fetch version metadata for the given mods. @@ -82,7 +93,7 @@ namespace StardewModdingAPI.Web.Controllers // fetch mod info result[modKey] = await this.Cache.GetOrCreateAsync($"{repository.VendorKey}:{modID}".ToLower(), async entry => { - entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddHours(1); + entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(this.CacheMinutes); return await repository.GetModInfoAsync(modID); }); } diff --git a/src/StardewModdingAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs b/src/StardewModdingAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs new file mode 100644 index 00000000..c8763800 --- /dev/null +++ b/src/StardewModdingAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs @@ -0,0 +1,21 @@ +namespace StardewModdingAPI.Web.Framework.ConfigModels +{ + /// The config settings for mod update checks. + public class ModUpdateCheckConfig + { + /// The number of minutes update checks should be cached before refetching them. + public int CacheMinutes { get; set; } + + /// The repository key for Nexus Mods. + public string NexusKey { get; set; } + + /// The user agent for the Nexus Mods API client. + public string NexusUserAgent { get; set; } + + /// The base URL for the Nexus Mods API. + public string NexusBaseUrl { get; set; } + + /// The URL for a Nexus Mods API query excluding the , where {0} is the mod ID. + public string NexusModUrlFormat { get; set; } + } +} diff --git a/src/StardewModdingAPI.Web/Framework/ModRepositories/NexusRepository.cs b/src/StardewModdingAPI.Web/Framework/ModRepositories/NexusRepository.cs index 02c2939a..312058ae 100644 --- a/src/StardewModdingAPI.Web/Framework/ModRepositories/NexusRepository.cs +++ b/src/StardewModdingAPI.Web/Framework/ModRepositories/NexusRepository.cs @@ -20,17 +20,25 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories ** Accessors *********/ /// The unique key for this vendor. - public string VendorKey { get; } = "Nexus"; + public string VendorKey { get; } + + /// The URL for a Nexus Mods API query excluding the base URL, where {0} is the mod ID. + public string ModUrlFormat { get; } /********* ** Public methods *********/ /// Construct an instance. - public NexusRepository() + /// The unique key for this vendor. + /// The user agent for the Nexus Mods API client. + /// The base URL for the Nexus Mods API. + /// The URL for a Nexus Mods API query excluding the , where {0} is the mod ID. + public NexusRepository(string vendorKey, string userAgent, string baseUrl, string modUrlFormat) { - this.Client = new FluentClient("http://www.nexusmods.com/stardewvalley") - .SetUserAgent("Nexus Client v0.63.15"); + this.VendorKey = vendorKey; + this.ModUrlFormat = modUrlFormat; + this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent); } /// Get metadata about a mod in the repository. @@ -40,7 +48,7 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories try { NexusResponseModel response = await this.Client - .GetAsync($"mods/{id}") + .GetAsync(string.Format(this.ModUrlFormat, id)) .As(); return response != null diff --git a/src/StardewModdingAPI.Web/Startup.cs b/src/StardewModdingAPI.Web/Startup.cs index 8aba6a5a..fbe2bd92 100644 --- a/src/StardewModdingAPI.Web/Startup.cs +++ b/src/StardewModdingAPI.Web/Startup.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using StardewModdingAPI.Web.Framework; +using StardewModdingAPI.Web.Framework.ConfigModels; namespace StardewModdingAPI.Web { @@ -39,6 +40,7 @@ namespace StardewModdingAPI.Web public void ConfigureServices(IServiceCollection services) { services + .Configure(this.Configuration.GetSection("ModUpdateCheck")) .AddMemoryCache() .AddMvc() .AddJsonOptions(options => diff --git a/src/StardewModdingAPI.Web/appsettings.json b/src/StardewModdingAPI.Web/appsettings.json index 5fff67ba..1e624055 100644 --- a/src/StardewModdingAPI.Web/appsettings.json +++ b/src/StardewModdingAPI.Web/appsettings.json @@ -1,8 +1,15 @@ -{ +{ "Logging": { "IncludeScopes": false, "LogLevel": { "Default": "Warning" } - } + }, + "ModUpdateCheck": { + "CacheMinutes": 60, + "NexusKey": "Nexus", + "NexusUserAgent": "Nexus Client v0.63.15", + "NexusBaseUrl": "http://www.nexusmods.com/stardewvalley", + "NexusModUrlFormat": "mods/{0}" + } } From 71d85a0c22a9295a08acdf433ef67d8b1b47e57b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 22 Sep 2017 22:11:04 -0400 Subject: [PATCH 084/186] restrict version to a valid SMAPI version (#336) --- .../Controllers/ModsController.cs | 5 ++--- src/StardewModdingAPI.Web/Startup.cs | 17 ++++++++++++++++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/StardewModdingAPI.Web/Controllers/ModsController.cs b/src/StardewModdingAPI.Web/Controllers/ModsController.cs index 50c23b99..a3f5001a 100644 --- a/src/StardewModdingAPI.Web/Controllers/ModsController.cs +++ b/src/StardewModdingAPI.Web/Controllers/ModsController.cs @@ -12,7 +12,6 @@ using StardewModdingAPI.Web.Models; namespace StardewModdingAPI.Web.Controllers { /// Provides an API to perform mod update checks. - [Route("api/v1.0/mods")] [Produces("application/json")] public class ModsController : Controller { @@ -55,7 +54,7 @@ namespace StardewModdingAPI.Web.Controllers [HttpGet] public async Task> GetAsync(string modKeys) { - return await this.Post(new ModSearchModel + return await this.PostAsync(new ModSearchModel { ModKeys = modKeys?.Split(',').Select(p => p.Trim()).ToArray() ?? new string[0] }); @@ -64,7 +63,7 @@ namespace StardewModdingAPI.Web.Controllers /// Fetch version metadata for the given mods. /// The search options. [HttpPost] - public async Task> Post([FromBody] ModSearchModel search) + public async Task> PostAsync([FromBody] ModSearchModel search) { // sort & filter keys string[] modKeys = (search.ModKeys ?? new string[0]) diff --git a/src/StardewModdingAPI.Web/Startup.cs b/src/StardewModdingAPI.Web/Startup.cs index fbe2bd92..b668c63e 100644 --- a/src/StardewModdingAPI.Web/Startup.cs +++ b/src/StardewModdingAPI.Web/Startup.cs @@ -60,7 +60,22 @@ namespace StardewModdingAPI.Web loggerFactory.AddDebug(); app .UseRewriter(new RewriteOptions().Add(new RewriteSubdomainRule())) // convert subdomain.smapi.io => smapi.io/subdomain for routing - .UseMvc(); + .UseMvc(route => + { + route.MapRoute( + name: "API", + template: "api/{version}/{controller}/{action?}", + defaults: new + { + action = "GetAsync" + }, + constraints: new + { + // version regex from SMAPI's SemanticVersion implementation + version = @"^v(?>(?0|[1-9]\d*))\.(?>(?0|[1-9]\d*))(?>(?:\.(?0|[1-9]\d*))?)(?:-(?(?>[a-z0-9]+[\-\.]?)+))?$" + } + ); + }); } } } From 0d6f6a9acef175fd9ea0df6790111d8d58d7f368 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 22 Sep 2017 22:42:04 -0400 Subject: [PATCH 085/186] add GitHub update check support (#336) --- .../Controllers/ModsController.cs | 1 + .../ConfigModels/ModUpdateCheckConfig.cs | 27 ++++++ .../ModRepositories/GitHubRepository.cs | 91 +++++++++++++++++++ src/StardewModdingAPI.Web/appsettings.json | 7 ++ 4 files changed, 126 insertions(+) create mode 100644 src/StardewModdingAPI.Web/Framework/ModRepositories/GitHubRepository.cs diff --git a/src/StardewModdingAPI.Web/Controllers/ModsController.cs b/src/StardewModdingAPI.Web/Controllers/ModsController.cs index a3f5001a..4b1abde4 100644 --- a/src/StardewModdingAPI.Web/Controllers/ModsController.cs +++ b/src/StardewModdingAPI.Web/Controllers/ModsController.cs @@ -44,6 +44,7 @@ namespace StardewModdingAPI.Web.Controllers this.Repositories = new IModRepository[] { + new GitHubRepository(config.GitHubKey, config.GitHubBaseUrl, config.GitHubReleaseUrlFormat, config.GitHubUserAgent, config.GitHubAcceptHeader), new NexusRepository(config.NexusKey, config.NexusUserAgent, config.NexusBaseUrl, config.NexusModUrlFormat) } .ToDictionary(p => p.VendorKey, StringComparer.CurrentCultureIgnoreCase); diff --git a/src/StardewModdingAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs b/src/StardewModdingAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs index c8763800..4dbad506 100644 --- a/src/StardewModdingAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs +++ b/src/StardewModdingAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs @@ -3,9 +3,36 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels /// The config settings for mod update checks. public class ModUpdateCheckConfig { + /********* + ** Accessors + *********/ + /**** + ** General + ****/ /// The number of minutes update checks should be cached before refetching them. public int CacheMinutes { get; set; } + /**** + ** GitHub + ****/ + /// The repository key for Nexus Mods. + public string GitHubKey { get; set; } + + /// The user agent for the GitHub API client. + public string GitHubUserAgent { get; set; } + + /// The base URL for the GitHub API. + public string GitHubBaseUrl { get; set; } + + /// The URL for a GitHub API latest-release query excluding the , where {0} is the organisation and project name. + public string GitHubReleaseUrlFormat { get; set; } + + /// The Accept header value expected by the GitHub API. + public string GitHubAcceptHeader { get; set; } + + /**** + ** Nexus Mods + ****/ /// The repository key for Nexus Mods. public string NexusKey { get; set; } diff --git a/src/StardewModdingAPI.Web/Framework/ModRepositories/GitHubRepository.cs b/src/StardewModdingAPI.Web/Framework/ModRepositories/GitHubRepository.cs new file mode 100644 index 00000000..c5772ad9 --- /dev/null +++ b/src/StardewModdingAPI.Web/Framework/ModRepositories/GitHubRepository.cs @@ -0,0 +1,91 @@ +using System; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Pathoschild.Http.Client; +using StardewModdingAPI.Web.Models; + +namespace StardewModdingAPI.Web.Framework.ModRepositories +{ + /// An HTTP client for fetching mod metadata from GitHub project releases. + internal class GitHubRepository : IModRepository + { + /********* + ** Properties + *********/ + /// The underlying HTTP client. + private readonly IClient Client; + + + /********* + ** Accessors + *********/ + /// The unique key for this vendor. + public string VendorKey { get; } + + /// The URL for a Nexus Mods API query excluding the base URL, where {0} is the mod ID. + public string ReleaseUrlFormat { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The unique key for this vendor. + /// The base URL for the Nexus Mods API. + /// The URL for a Nexus Mods API query excluding the , where {0} is the mod ID. + /// The user agent for the GitHub API client. + /// The Accept header value expected by the GitHub API. + public GitHubRepository(string vendorKey, string baseUrl, string releaseUrlFormat, string userAgent, string acceptHeader) + { + this.VendorKey = vendorKey; + this.ReleaseUrlFormat = releaseUrlFormat; + + this.Client = new FluentClient(baseUrl) + .SetUserAgent(string.Format(userAgent, this.GetType().Assembly.GetName().Version)) + .AddDefault(req => req.WithHeader("Accept", acceptHeader)); + } + + /// Get metadata about a mod in the repository. + /// The mod ID in this repository. + public async Task GetModInfoAsync(string id) + { + try + { + GitRelease release = await this.Client + .GetAsync(string.Format(this.ReleaseUrlFormat, id)) + .As(); + + return new ModInfoModel(id, release.Tag, $"https://github.com/{id}/releases"); + } + catch (Exception ex) + { + return new ModInfoModel(ex.ToString()); + } + } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public void Dispose() + { + this.Client.Dispose(); + } + + + /********* + ** Private models + *********/ + /// Metadata about a GitHub release tag. + private class GitRelease + { + /********* + ** Accessors + *********/ + /// The display name. + [JsonProperty("name")] + public string Name { get; set; } + + /// The semantic version string. + [JsonProperty("tag_name")] + public string Tag { get; set; } + } + } +} diff --git a/src/StardewModdingAPI.Web/appsettings.json b/src/StardewModdingAPI.Web/appsettings.json index 1e624055..f996157c 100644 --- a/src/StardewModdingAPI.Web/appsettings.json +++ b/src/StardewModdingAPI.Web/appsettings.json @@ -7,6 +7,13 @@ }, "ModUpdateCheck": { "CacheMinutes": 60, + + "GitHubKey": "GitHub", + "GitHubUserAgent": "SMAPI/{0} (+https://github.com/Pathoschild/SMAPI)", + "GitHubBaseUrl": "https://api.github.com", + "GitHubReleaseUrlFormat": "repos/{0}/releases/latest", + "GitHubAcceptHeader": "application/vnd.github.v3+json", + "NexusKey": "Nexus", "NexusUserAgent": "Nexus Client v0.63.15", "NexusBaseUrl": "http://www.nexusmods.com/stardewvalley", From eaabd91f31db35e050b7215f5f36568bf6982a83 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 22 Sep 2017 23:43:58 -0400 Subject: [PATCH 086/186] authenticate GitHub queries if auth details are configured (#336) --- .../Controllers/ModsController.cs | 17 +++++++++++++++-- .../ConfigModels/ModUpdateCheckConfig.cs | 6 ++++++ .../ModRepositories/GitHubRepository.cs | 6 +++++- src/StardewModdingAPI.Web/Startup.cs | 1 + src/StardewModdingAPI.Web/appsettings.json | 2 ++ 5 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/StardewModdingAPI.Web/Controllers/ModsController.cs b/src/StardewModdingAPI.Web/Controllers/ModsController.cs index 4b1abde4..4715d379 100644 --- a/src/StardewModdingAPI.Web/Controllers/ModsController.cs +++ b/src/StardewModdingAPI.Web/Controllers/ModsController.cs @@ -44,8 +44,21 @@ namespace StardewModdingAPI.Web.Controllers this.Repositories = new IModRepository[] { - new GitHubRepository(config.GitHubKey, config.GitHubBaseUrl, config.GitHubReleaseUrlFormat, config.GitHubUserAgent, config.GitHubAcceptHeader), - new NexusRepository(config.NexusKey, config.NexusUserAgent, config.NexusBaseUrl, config.NexusModUrlFormat) + new GitHubRepository( + vendorKey: config.GitHubKey, + baseUrl: config.GitHubBaseUrl, + releaseUrlFormat: config.GitHubReleaseUrlFormat, + userAgent: config.GitHubUserAgent, + acceptHeader: config.GitHubAcceptHeader, + username: config.GitHubUsername, + password: config.GitHubPassword + ), + new NexusRepository( + vendorKey: config.NexusKey, + userAgent: config.NexusUserAgent, + baseUrl: config.NexusBaseUrl, + modUrlFormat: config.NexusModUrlFormat + ) } .ToDictionary(p => p.VendorKey, StringComparer.CurrentCultureIgnoreCase); } diff --git a/src/StardewModdingAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs b/src/StardewModdingAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs index 4dbad506..5d55ba18 100644 --- a/src/StardewModdingAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs +++ b/src/StardewModdingAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs @@ -30,6 +30,12 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels /// The Accept header value expected by the GitHub API. public string GitHubAcceptHeader { get; set; } + /// The username with which to authenticate to the GitHub API (if any). + public string GitHubUsername { get; set; } + + /// The password with which to authenticate to the GitHub API (if any). + public string GitHubPassword { get; set; } + /**** ** Nexus Mods ****/ diff --git a/src/StardewModdingAPI.Web/Framework/ModRepositories/GitHubRepository.cs b/src/StardewModdingAPI.Web/Framework/ModRepositories/GitHubRepository.cs index c5772ad9..67e706ed 100644 --- a/src/StardewModdingAPI.Web/Framework/ModRepositories/GitHubRepository.cs +++ b/src/StardewModdingAPI.Web/Framework/ModRepositories/GitHubRepository.cs @@ -35,7 +35,9 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories /// The URL for a Nexus Mods API query excluding the , where {0} is the mod ID. /// The user agent for the GitHub API client. /// The Accept header value expected by the GitHub API. - public GitHubRepository(string vendorKey, string baseUrl, string releaseUrlFormat, string userAgent, string acceptHeader) + /// The username with which to authenticate to the GitHub API. + /// The password with which to authenticate to the GitHub API. + public GitHubRepository(string vendorKey, string baseUrl, string releaseUrlFormat, string userAgent, string acceptHeader, string username, string password) { this.VendorKey = vendorKey; this.ReleaseUrlFormat = releaseUrlFormat; @@ -43,6 +45,8 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories this.Client = new FluentClient(baseUrl) .SetUserAgent(string.Format(userAgent, this.GetType().Assembly.GetName().Version)) .AddDefault(req => req.WithHeader("Accept", acceptHeader)); + if (!string.IsNullOrWhiteSpace(username)) + this.Client = this.Client.SetBasicAuthentication(username, password); } /// Get metadata about a mod in the repository. diff --git a/src/StardewModdingAPI.Web/Startup.cs b/src/StardewModdingAPI.Web/Startup.cs index b668c63e..4806aefe 100644 --- a/src/StardewModdingAPI.Web/Startup.cs +++ b/src/StardewModdingAPI.Web/Startup.cs @@ -29,6 +29,7 @@ namespace StardewModdingAPI.Web { this.Configuration = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) + .AddEnvironmentVariables() .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) .AddEnvironmentVariables() diff --git a/src/StardewModdingAPI.Web/appsettings.json b/src/StardewModdingAPI.Web/appsettings.json index f996157c..29fb195e 100644 --- a/src/StardewModdingAPI.Web/appsettings.json +++ b/src/StardewModdingAPI.Web/appsettings.json @@ -13,6 +13,8 @@ "GitHubBaseUrl": "https://api.github.com", "GitHubReleaseUrlFormat": "repos/{0}/releases/latest", "GitHubAcceptHeader": "application/vnd.github.v3+json", + "GitHubUsername": null, /* set via environment properties */ + "GitHubPassword": null, /* set via environment properties */ "NexusKey": "Nexus", "NexusUserAgent": "Nexus Client v0.63.15", From c2d8760c563da5f6182cb9475a1bb12c8622f455 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 23 Sep 2017 14:15:59 -0400 Subject: [PATCH 087/186] make web controllers internal (#336) This is needed to support internal models, which is needed to share the models with the main SMAPI assembly without making them visible to mods. --- .../Controllers/ModsController.cs | 2 +- .../InternalControllerFeatureProvider.cs | 27 +++++++++++++++++++ .../Framework/RewriteSubdomainRule.cs | 2 +- .../Models/ModInfoModel.cs | 2 +- .../Models/ModSearchModel.cs | 2 +- src/StardewModdingAPI.Web/Startup.cs | 3 ++- 6 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 src/StardewModdingAPI.Web/Framework/InternalControllerFeatureProvider.cs diff --git a/src/StardewModdingAPI.Web/Controllers/ModsController.cs b/src/StardewModdingAPI.Web/Controllers/ModsController.cs index 4715d379..79c31c3f 100644 --- a/src/StardewModdingAPI.Web/Controllers/ModsController.cs +++ b/src/StardewModdingAPI.Web/Controllers/ModsController.cs @@ -13,7 +13,7 @@ namespace StardewModdingAPI.Web.Controllers { /// Provides an API to perform mod update checks. [Produces("application/json")] - public class ModsController : Controller + internal class ModsController : Controller { /********* ** Properties diff --git a/src/StardewModdingAPI.Web/Framework/InternalControllerFeatureProvider.cs b/src/StardewModdingAPI.Web/Framework/InternalControllerFeatureProvider.cs new file mode 100644 index 00000000..2c24c610 --- /dev/null +++ b/src/StardewModdingAPI.Web/Framework/InternalControllerFeatureProvider.cs @@ -0,0 +1,27 @@ +using System; +using System.Reflection; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; + +namespace StardewModdingAPI.Web.Framework +{ + /// Discovers controllers with support for non-public controllers. + internal class InternalControllerFeatureProvider : ControllerFeatureProvider + { + /********* + ** Public methods + *********/ + /// Determines if a given type is a controller. + /// The candidate. + /// true if the type is a controller; otherwise false. + protected override bool IsController(TypeInfo type) + { + return + type.IsClass + && !type.IsAbstract + && (/*type.IsPublic &&*/ !type.ContainsGenericParameters) + && (!type.IsDefined(typeof(NonControllerAttribute)) + && (type.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) || type.IsDefined(typeof(ControllerAttribute)))); + } + } +} diff --git a/src/StardewModdingAPI.Web/Framework/RewriteSubdomainRule.cs b/src/StardewModdingAPI.Web/Framework/RewriteSubdomainRule.cs index 9b89cb65..5a56844f 100644 --- a/src/StardewModdingAPI.Web/Framework/RewriteSubdomainRule.cs +++ b/src/StardewModdingAPI.Web/Framework/RewriteSubdomainRule.cs @@ -5,7 +5,7 @@ namespace StardewModdingAPI.Web.Framework { /// Rewrite requests to prepend the subdomain portion (if any) to the path. /// Derived from . - public class RewriteSubdomainRule : IRule + internal class RewriteSubdomainRule : IRule { /// Applies the rule. Implementations of ApplyRule should set the value for (defaults to RuleResult.ContinueRules). /// The rewrite context. diff --git a/src/StardewModdingAPI.Web/Models/ModInfoModel.cs b/src/StardewModdingAPI.Web/Models/ModInfoModel.cs index 180420cd..4e9762e5 100644 --- a/src/StardewModdingAPI.Web/Models/ModInfoModel.cs +++ b/src/StardewModdingAPI.Web/Models/ModInfoModel.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace StardewModdingAPI.Web.Models { /// Generic metadata about a mod. - public class ModInfoModel + internal class ModInfoModel { /********* ** Accessors diff --git a/src/StardewModdingAPI.Web/Models/ModSearchModel.cs b/src/StardewModdingAPI.Web/Models/ModSearchModel.cs index b5bd12fb..b9a73af6 100644 --- a/src/StardewModdingAPI.Web/Models/ModSearchModel.cs +++ b/src/StardewModdingAPI.Web/Models/ModSearchModel.cs @@ -1,7 +1,7 @@ namespace StardewModdingAPI.Web.Models { /// Metadata for mods to look up. - public class ModSearchModel + internal class ModSearchModel { /// The namespaced mod keys to search. public string[] ModKeys { get; set; } diff --git a/src/StardewModdingAPI.Web/Startup.cs b/src/StardewModdingAPI.Web/Startup.cs index 4806aefe..d5b828b7 100644 --- a/src/StardewModdingAPI.Web/Startup.cs +++ b/src/StardewModdingAPI.Web/Startup.cs @@ -11,7 +11,7 @@ using StardewModdingAPI.Web.Framework.ConfigModels; namespace StardewModdingAPI.Web { /// The web app startup configuration. - public class Startup + internal class Startup { /********* ** Accessors @@ -44,6 +44,7 @@ namespace StardewModdingAPI.Web .Configure(this.Configuration.GetSection("ModUpdateCheck")) .AddMemoryCache() .AddMvc() + .ConfigureApplicationPartManager(manager => manager.FeatureProviders.Add(new InternalControllerFeatureProvider())) .AddJsonOptions(options => { options.SerializerSettings.Formatting = Formatting.Indented; From 9ffe0bd37100336933a9768ceca2e6a65bdc0a58 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 23 Sep 2017 14:19:10 -0400 Subject: [PATCH 088/186] switch to the mods GET endpoint (#336) --- .../Controllers/ModsController.cs | 15 ++------------- .../Models/ModSearchModel.cs | 9 --------- 2 files changed, 2 insertions(+), 22 deletions(-) delete mode 100644 src/StardewModdingAPI.Web/Models/ModSearchModel.cs diff --git a/src/StardewModdingAPI.Web/Controllers/ModsController.cs b/src/StardewModdingAPI.Web/Controllers/ModsController.cs index 79c31c3f..06a80638 100644 --- a/src/StardewModdingAPI.Web/Controllers/ModsController.cs +++ b/src/StardewModdingAPI.Web/Controllers/ModsController.cs @@ -67,27 +67,16 @@ namespace StardewModdingAPI.Web.Controllers /// The namespaced mod keys to search as a comma-delimited array. [HttpGet] public async Task> GetAsync(string modKeys) - { - return await this.PostAsync(new ModSearchModel - { - ModKeys = modKeys?.Split(',').Select(p => p.Trim()).ToArray() ?? new string[0] - }); - } - - /// Fetch version metadata for the given mods. - /// The search options. - [HttpPost] - public async Task> PostAsync([FromBody] ModSearchModel search) { // sort & filter keys - string[] modKeys = (search.ModKeys ?? new string[0]) + string[] modKeysArray = (modKeys?.Split(',').Select(p => p.Trim()).ToArray() ?? new string[0]) .Distinct(StringComparer.CurrentCultureIgnoreCase) .OrderBy(p => p, StringComparer.CurrentCultureIgnoreCase) .ToArray(); // fetch mod info IDictionary result = new Dictionary(StringComparer.CurrentCultureIgnoreCase); - foreach (string modKey in modKeys) + foreach (string modKey in modKeysArray) { // parse mod key if (!this.TryParseModKey(modKey, out string vendorKey, out string modID)) diff --git a/src/StardewModdingAPI.Web/Models/ModSearchModel.cs b/src/StardewModdingAPI.Web/Models/ModSearchModel.cs deleted file mode 100644 index b9a73af6..00000000 --- a/src/StardewModdingAPI.Web/Models/ModSearchModel.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace StardewModdingAPI.Web.Models -{ - /// Metadata for mods to look up. - internal class ModSearchModel - { - /// The namespaced mod keys to search. - public string[] ModKeys { get; set; } - } -} From bdee7f88e9ddbab51869431efbff90100b65bbd1 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 23 Sep 2017 14:37:58 -0400 Subject: [PATCH 089/186] move API models into shared project for reuse (#336) --- .../ModInfoModel.cs | 2 +- .../StardewModdingAPI.Models.projitems | 14 ++++++++++++++ .../StardewModdingAPI.Models.shproj | 13 +++++++++++++ .../Controllers/ModsController.cs | 2 +- .../Framework/ModRepositories/GitHubRepository.cs | 2 +- .../Framework/ModRepositories/IModRepository.cs | 2 +- .../Framework/ModRepositories/NexusRepository.cs | 2 +- .../StardewModdingAPI.Web.csproj | 1 + src/StardewModdingAPI.sln | 13 ++++++++++++- 9 files changed, 45 insertions(+), 6 deletions(-) rename src/{StardewModdingAPI.Web/Models => StardewModdingAPI.Models}/ModInfoModel.cs (97%) create mode 100644 src/StardewModdingAPI.Models/StardewModdingAPI.Models.projitems create mode 100644 src/StardewModdingAPI.Models/StardewModdingAPI.Models.shproj diff --git a/src/StardewModdingAPI.Web/Models/ModInfoModel.cs b/src/StardewModdingAPI.Models/ModInfoModel.cs similarity index 97% rename from src/StardewModdingAPI.Web/Models/ModInfoModel.cs rename to src/StardewModdingAPI.Models/ModInfoModel.cs index 4e9762e5..44071230 100644 --- a/src/StardewModdingAPI.Web/Models/ModInfoModel.cs +++ b/src/StardewModdingAPI.Models/ModInfoModel.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace StardewModdingAPI.Web.Models +namespace StardewModdingAPI.Models { /// Generic metadata about a mod. internal class ModInfoModel diff --git a/src/StardewModdingAPI.Models/StardewModdingAPI.Models.projitems b/src/StardewModdingAPI.Models/StardewModdingAPI.Models.projitems new file mode 100644 index 00000000..2465760e --- /dev/null +++ b/src/StardewModdingAPI.Models/StardewModdingAPI.Models.projitems @@ -0,0 +1,14 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + 2aa02fb6-ff03-41cf-a215-2ee60ab4f5dc + + + StardewModdingAPI.Models + + + + + \ No newline at end of file diff --git a/src/StardewModdingAPI.Models/StardewModdingAPI.Models.shproj b/src/StardewModdingAPI.Models/StardewModdingAPI.Models.shproj new file mode 100644 index 00000000..c80517af --- /dev/null +++ b/src/StardewModdingAPI.Models/StardewModdingAPI.Models.shproj @@ -0,0 +1,13 @@ + + + + 2aa02fb6-ff03-41cf-a215-2ee60ab4f5dc + 14.0 + + + + + + + + diff --git a/src/StardewModdingAPI.Web/Controllers/ModsController.cs b/src/StardewModdingAPI.Web/Controllers/ModsController.cs index 06a80638..8fc2cb51 100644 --- a/src/StardewModdingAPI.Web/Controllers/ModsController.cs +++ b/src/StardewModdingAPI.Web/Controllers/ModsController.cs @@ -7,7 +7,7 @@ using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using StardewModdingAPI.Web.Framework.ConfigModels; using StardewModdingAPI.Web.Framework.ModRepositories; -using StardewModdingAPI.Web.Models; +using StardewModdingAPI.Models; namespace StardewModdingAPI.Web.Controllers { diff --git a/src/StardewModdingAPI.Web/Framework/ModRepositories/GitHubRepository.cs b/src/StardewModdingAPI.Web/Framework/ModRepositories/GitHubRepository.cs index 67e706ed..421220de 100644 --- a/src/StardewModdingAPI.Web/Framework/ModRepositories/GitHubRepository.cs +++ b/src/StardewModdingAPI.Web/Framework/ModRepositories/GitHubRepository.cs @@ -2,7 +2,7 @@ using System; using System.Threading.Tasks; using Newtonsoft.Json; using Pathoschild.Http.Client; -using StardewModdingAPI.Web.Models; +using StardewModdingAPI.Models; namespace StardewModdingAPI.Web.Framework.ModRepositories { diff --git a/src/StardewModdingAPI.Web/Framework/ModRepositories/IModRepository.cs b/src/StardewModdingAPI.Web/Framework/ModRepositories/IModRepository.cs index 7fd735cd..98e4c957 100644 --- a/src/StardewModdingAPI.Web/Framework/ModRepositories/IModRepository.cs +++ b/src/StardewModdingAPI.Web/Framework/ModRepositories/IModRepository.cs @@ -1,6 +1,6 @@ using System; using System.Threading.Tasks; -using StardewModdingAPI.Web.Models; +using StardewModdingAPI.Models; namespace StardewModdingAPI.Web.Framework.ModRepositories { diff --git a/src/StardewModdingAPI.Web/Framework/ModRepositories/NexusRepository.cs b/src/StardewModdingAPI.Web/Framework/ModRepositories/NexusRepository.cs index 312058ae..6cf5b04a 100644 --- a/src/StardewModdingAPI.Web/Framework/ModRepositories/NexusRepository.cs +++ b/src/StardewModdingAPI.Web/Framework/ModRepositories/NexusRepository.cs @@ -2,7 +2,7 @@ using System; using System.Threading.Tasks; using Newtonsoft.Json; using Pathoschild.Http.Client; -using StardewModdingAPI.Web.Models; +using StardewModdingAPI.Models; namespace StardewModdingAPI.Web.Framework.ModRepositories { diff --git a/src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj b/src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj index 2af7c3df..c30abc55 100644 --- a/src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj +++ b/src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj @@ -15,5 +15,6 @@ + diff --git a/src/StardewModdingAPI.sln b/src/StardewModdingAPI.sln index 031e2d5f..bc2bb8b2 100644 --- a/src/StardewModdingAPI.sln +++ b/src/StardewModdingAPI.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26730.15 +VisualStudioVersion = 15.0.26730.16 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrainerMod", "TrainerMod\TrainerMod.csproj", "{28480467-1A48-46A7-99F8-236D95225359}" EndProject @@ -31,7 +31,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Tests", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StardewModdingAPI.Web", "StardewModdingAPI.Web\StardewModdingAPI.Web.csproj", "{A308F679-51A3-4006-92D5-BAEC7EBD01A1}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Internal", "Internal", "{82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11}" +EndProject +Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "StardewModdingAPI.Models", "StardewModdingAPI.Models\StardewModdingAPI.Models.shproj", "{2AA02FB6-FF03-41CF-A215-2EE60AB4F5DC}" +EndProject Global + GlobalSection(SharedMSBuildProjectFiles) = preSolution + StardewModdingAPI.Models\StardewModdingAPI.Models.projitems*{2aa02fb6-ff03-41cf-a215-2ee60ab4f5dc}*SharedItemsImports = 13 + EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Debug|Mixed Platforms = Debug|Mixed Platforms @@ -97,6 +104,10 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {36CCB19E-92EB-48C7-9615-98EEFD45109B} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} + {2AA02FB6-FF03-41CF-A215-2EE60AB4F5DC} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {70143042-A862-47A8-A677-7C819DDC90DC} EndGlobalSection From e178ed14be24e3971d50addaebb3d13c19c18304 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 23 Sep 2017 18:04:56 -0400 Subject: [PATCH 090/186] migrate SMAPI update check to new web API (#336) --- src/StardewModdingAPI.sln | 1 + .../Framework/Models/GitRelease.cs | 19 ------ .../Framework/Models/SConfig.cs | 14 ++-- .../Framework/UpdateHelper.cs | 65 ++++++++++++++----- src/StardewModdingAPI/Program.cs | 12 ++-- .../StardewModdingAPI.config.json | 13 +++- .../StardewModdingAPI.csproj | 2 +- 7 files changed, 81 insertions(+), 45 deletions(-) delete mode 100644 src/StardewModdingAPI/Framework/Models/GitRelease.cs diff --git a/src/StardewModdingAPI.sln b/src/StardewModdingAPI.sln index bc2bb8b2..9d7baa51 100644 --- a/src/StardewModdingAPI.sln +++ b/src/StardewModdingAPI.sln @@ -38,6 +38,7 @@ EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution StardewModdingAPI.Models\StardewModdingAPI.Models.projitems*{2aa02fb6-ff03-41cf-a215-2ee60ab4f5dc}*SharedItemsImports = 13 + StardewModdingAPI.Models\StardewModdingAPI.Models.projitems*{f1a573b0-f436-472c-ae29-0b91ea6b9f8f}*SharedItemsImports = 4 EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/src/StardewModdingAPI/Framework/Models/GitRelease.cs b/src/StardewModdingAPI/Framework/Models/GitRelease.cs deleted file mode 100644 index bc53468f..00000000 --- a/src/StardewModdingAPI/Framework/Models/GitRelease.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Newtonsoft.Json; - -namespace StardewModdingAPI.Framework.Models -{ - /// Metadata about a GitHub release tag. - internal class GitRelease - { - /********* - ** Accessors - *********/ - /// The display name. - [JsonProperty("name")] - public string Name { get; set; } - - /// The semantic version string. - [JsonProperty("tag_name")] - public string Tag { get; set; } - } -} \ No newline at end of file diff --git a/src/StardewModdingAPI/Framework/Models/SConfig.cs b/src/StardewModdingAPI/Framework/Models/SConfig.cs index b2ca4113..36799400 100644 --- a/src/StardewModdingAPI/Framework/Models/SConfig.cs +++ b/src/StardewModdingAPI/Framework/Models/SConfig.cs @@ -1,4 +1,4 @@ -namespace StardewModdingAPI.Framework.Models +namespace StardewModdingAPI.Framework.Models { /// The SMAPI configuration settings. internal class SConfig @@ -9,11 +9,17 @@ /// Whether to enable development features. public bool DeveloperMode { get; set; } - /// Whether to check if a newer version of SMAPI is available on startup. - public bool CheckForUpdates { get; set; } = true; + /// Whether to check for newer versions of SMAPI and mods on startup. + public bool CheckForUpdates { get; set; } + + /// SMAPI's GitHub project name, used to perform update checks. + public string GitHubProjectName { get; set; } + + /// The base URL for SMAPI's web API, used to perform update checks. + public string WebApiBaseUrl { get; set; } /// Whether SMAPI should log more information about the game context. - public bool VerboseLogging { get; set; } = false; + public bool VerboseLogging { get; set; } /// A list of mod versions which should be considered compatible or incompatible regardless of whether SMAPI detects incompatible code. public ModCompatibility[] ModCompatibility { get; set; } diff --git a/src/StardewModdingAPI/Framework/UpdateHelper.cs b/src/StardewModdingAPI/Framework/UpdateHelper.cs index e01e55c8..0ee57648 100644 --- a/src/StardewModdingAPI/Framework/UpdateHelper.cs +++ b/src/StardewModdingAPI/Framework/UpdateHelper.cs @@ -1,36 +1,69 @@ -using System.IO; +using System; +using System.Collections.Generic; +using System.IO; using System.Net; -using System.Reflection; using System.Threading.Tasks; using Newtonsoft.Json; -using StardewModdingAPI.Framework.Models; +using StardewModdingAPI.Models; namespace StardewModdingAPI.Framework { - /// Provides utility methods for mod updates. - internal class UpdateHelper + /// Provides methods for interacting with the SMAPI web API. + internal class WebApiClient { + /********* + ** Properties + *********/ + /// The base URL for the web API. + private readonly Uri BaseUrl; + + /// The API version number. + private readonly ISemanticVersion Version; + + /********* ** Public methods *********/ - /// Get the latest release from a GitHub repository. - /// The name of the repository from which to fetch releases (like "cjsu/SMAPI"). - public static async Task GetLatestVersionAsync(string repository) + /// Construct an instance. + /// The base URL for the web API. + /// The web API version. + public WebApiClient(string baseUrl, ISemanticVersion version) { - // build request - // (avoid HttpClient for Mac compatibility) - HttpWebRequest request = WebRequest.CreateHttp($"https://api.github.com/repos/{repository}/releases/latest"); - AssemblyName assembly = typeof(UpdateHelper).Assembly.GetName(); - request.UserAgent = $"{assembly.Name}/{assembly.Version}"; - request.Accept = "application/vnd.github.v3+json"; +#if !SMAPI_FOR_WINDOWS + baseUrl = baseUrl.Replace("https://", "http://"); // workaround for OpenSSL issues with the game's bundled Mono on Linux/Mac +#endif + this.BaseUrl = new Uri(baseUrl); + this.Version = version; + } - // fetch data + /// Get the latest SMAPI version. + /// The mod keys for which to fetch the latest version. + public async Task> GetModInfoAsync(params string[] modKeys) + { + string url = $"v{this.Version}/mods?modKeys={Uri.EscapeDataString(string.Join(",", modKeys))}"; + return await this.GetAsync>(url); + } + + + /********* + ** Private methods + *********/ + /// Fetch the response from the backend API. + /// The expected response type. + /// The request URL, optionally excluding the base URL. + private async Task GetAsync(string url) + { + // build request (avoid HttpClient for Mac compatibility) + HttpWebRequest request = WebRequest.CreateHttp(new Uri(this.BaseUrl, url).ToString()); + request.UserAgent = $"SMAPI/{this.Version}"; + + // fetch data using (WebResponse response = await request.GetResponseAsync()) using (Stream responseStream = response.GetResponseStream()) using (StreamReader reader = new StreamReader(responseStream)) { string responseText = reader.ReadToEnd(); - return JsonConvert.DeserializeObject(responseText); + return JsonConvert.DeserializeObject(responseText); } } } diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 84af2777..df94b02a 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -21,6 +21,7 @@ using StardewModdingAPI.Framework.ModHelpers; using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Serialisation; +using StardewModdingAPI.Models; using StardewValley; using Monitor = StardewModdingAPI.Framework.Monitor; using SObject = StardewValley.Object; @@ -572,10 +573,13 @@ namespace StardewModdingAPI { try { - GitRelease release = UpdateHelper.GetLatestVersionAsync(Constants.GitHubRepository).Result; - ISemanticVersion latestVersion = new SemanticVersion(release.Tag); - if (latestVersion.IsNewerThan(Constants.ApiVersion)) - this.Monitor.Log($"You can update SMAPI from version {Constants.ApiVersion} to {latestVersion}", LogLevel.Alert); + var client = new WebApiClient(this.Settings.WebApiBaseUrl, Constants.ApiVersion); + string key = $"GitHub:{this.Settings.GitHubProjectName}"; + ModInfoModel info = client.GetModInfoAsync(key).Result[key]; + if (info.Error != null) + this.Monitor.Log($"Couldn't check for a new version of SMAPI. This won't affect your game, but you may not be notified of new versions if this keeps happening.\n{info.Error}"); + else if (new SemanticVersion(info.Version).IsNewerThan(Constants.ApiVersion)) + this.Monitor.Log($"You can update SMAPI from version {Constants.ApiVersion} to {info.Version}.", LogLevel.Alert); } catch (Exception ex) { diff --git a/src/StardewModdingAPI/StardewModdingAPI.config.json b/src/StardewModdingAPI/StardewModdingAPI.config.json index d393f5a9..67d8f270 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.config.json +++ b/src/StardewModdingAPI/StardewModdingAPI.config.json @@ -1,4 +1,4 @@ -/* +/* @@ -21,6 +21,17 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha */ "CheckForUpdates": true, + /** + * SMAPI's GitHub project name, used to perform update checks. + */ + "GitHubProjectName": "Pathoschild/SMAPI", + + /** + * The base URL for SMAPI's web API, used to perform update checks. + * 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", + /** * Whether SMAPI should log more information about the game context. */ diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 8da93bf4..d5fffd3f 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -219,7 +219,6 @@ - @@ -274,6 +273,7 @@ false + \ No newline at end of file From 873abef23563f5273e9b66d7b7e3cc2f5e4e0e92 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 23 Sep 2017 19:15:07 -0400 Subject: [PATCH 091/186] add mod update checks based on manifest fields (#336) --- release-notes.md | 5 +- src/StardewModdingAPI/Events/GameEvents.cs | 10 +- .../Framework/Models/Manifest.cs | 10 +- src/StardewModdingAPI/Framework/SGame.cs | 2 +- src/StardewModdingAPI/IManifest.cs | 10 +- src/StardewModdingAPI/Program.cs | 107 ++++++++++++++++-- .../StardewModdingAPI.config.json | 6 +- 7 files changed, 123 insertions(+), 27 deletions(-) diff --git a/release-notes.md b/release-notes.md index c31d2bae..a07de71f 100644 --- a/release-notes.md +++ b/release-notes.md @@ -6,12 +6,15 @@ For players: * The console is now simpler and easier to read. * The console now adjusts its colors when you have a light terminal background. * SMAPI now detects mods which may impact game stability and shows a warning in the console. -* Updated compatibility list. +* SMAPI now alerts you in the console when one of your mods has a new version. * Renamed installer folder from `SMAPI 2.0` to `SMAPI 2.0 installer` to avoid confusion. +* Updated compatibility list. +* Fixed update check errors on Linux/Mac. For mod developers: * Added new APIs to edit, inject, and reload XNB assets loaded by the game at any time. _This let mods do anything previously only possible with XNB mods, plus enables new mod scenarios (e.g. seasonal textures, NPC clothing that depend on the weather or location, etc)._ +* Added new manifest fields to enable automatic update checks. * Added new input events. _The new `InputEvents` combine keyboard + mouse + controller input into one event for easy handling, add metadata like the cursor position and grab tile to support click handling, and add an option to suppress input from the game to enable new scenarios like action highjacking and UI overlays._ * Added support for optional dependencies. diff --git a/src/StardewModdingAPI/Events/GameEvents.cs b/src/StardewModdingAPI/Events/GameEvents.cs index 5610e67a..deb71a86 100644 --- a/src/StardewModdingAPI/Events/GameEvents.cs +++ b/src/StardewModdingAPI/Events/GameEvents.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics.CodeAnalysis; using StardewModdingAPI.Framework; @@ -39,9 +39,6 @@ namespace StardewModdingAPI.Events /// Raised during launch after configuring XNA or MonoGame. The game window hasn't been opened by this point. Called after . internal static event EventHandler InitializeInternal; - /// Raised during launch after configuring Stardew Valley, loading it into memory, and opening the game window. The window is still blank by this point. - internal static event EventHandler GameLoadedInternal; - #if SMAPI_1_x /// Raised during launch after configuring XNA or MonoGame. The game window hasn't been opened by this point. Called after . [Obsolete("The " + nameof(Mod) + "." + nameof(Mod.Entry) + " method is now called after the " + nameof(GameEvents.Initialize) + " event, so any contained logic can be done directly in " + nameof(Mod.Entry) + ".")] @@ -143,19 +140,14 @@ namespace StardewModdingAPI.Events { monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.LoadContent)}", GameEvents._LoadContent?.GetInvocationList()); } -#endif /// Raise a event. /// Encapsulates monitoring and logging. internal static void InvokeGameLoaded(IMonitor monitor) { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.GameLoadedInternal)}", GameEvents.GameLoadedInternal?.GetInvocationList()); -#if SMAPI_1_x monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.GameLoaded)}", GameEvents._GameLoaded?.GetInvocationList()); -#endif } -#if SMAPI_1_x /// Raise a event. /// Encapsulates monitoring and logging. internal static void InvokeFirstUpdateTick(IMonitor monitor) diff --git a/src/StardewModdingAPI/Framework/Models/Manifest.cs b/src/StardewModdingAPI/Framework/Models/Manifest.cs index 29c3517e..f97cb8ff 100644 --- a/src/StardewModdingAPI/Framework/Models/Manifest.cs +++ b/src/StardewModdingAPI/Framework/Models/Manifest.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Newtonsoft.Json; using StardewModdingAPI.Framework.Serialisation; @@ -35,6 +35,14 @@ namespace StardewModdingAPI.Framework.Models [JsonConverter(typeof(SFieldConverter))] public IManifestDependency[] Dependencies { get; set; } +#if !SMAPI_1_x + /// The mod's unique ID in Nexus Mods (if any), used for update checks. + public string NexusID { get; set; } + + /// The mod's organisation and project name on GitHub (if any), used for update checks. + public string GitHubProject { get; set; } +#endif + /// The unique mod ID. public string UniqueID { get; set; } diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index 76c106d7..387aeacc 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -297,8 +297,8 @@ namespace StardewModdingAPI.Framework GameEvents.InvokeInitialize(this.Monitor); #if SMAPI_1_x GameEvents.InvokeLoadContent(this.Monitor); -#endif GameEvents.InvokeGameLoaded(this.Monitor); +#endif } /********* diff --git a/src/StardewModdingAPI/IManifest.cs b/src/StardewModdingAPI/IManifest.cs index 407db1ce..28f6570c 100644 --- a/src/StardewModdingAPI/IManifest.cs +++ b/src/StardewModdingAPI/IManifest.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace StardewModdingAPI { @@ -32,7 +32,13 @@ namespace StardewModdingAPI /// The other mods that must be loaded before this mod. IManifestDependency[] Dependencies { get; } + /// The mod's unique ID in Nexus Mods (if any), used for update checks. + string NexusID { get; set; } + + /// The mod's organisation and project name on GitHub (if any), used for update checks. + string GitHubProject { get; set; } + /// Any manifest fields which didn't match a valid field. IDictionary ExtraFields { get; } } -} \ No newline at end of file +} diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index df94b02a..cee3aefd 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -188,7 +188,6 @@ namespace StardewModdingAPI #endif this.GameInstance.Exiting += (sender, e) => this.Dispose(); GameEvents.InitializeInternal += (sender, e) => this.InitialiseAfterGameStart(); - GameEvents.GameLoadedInternal += (sender, e) => this.CheckForUpdateAsync(); ContentEvents.AfterLocaleChanged += (sender, e) => this.OnLocaleChanged(); // set window titles @@ -437,6 +436,9 @@ namespace StardewModdingAPI #else this.LoadMods(mods, new JsonHelper(), this.ContentManager); #endif + + // check for updates + this.CheckForUpdatesAsync(mods); } if (this.Monitor.IsExiting) { @@ -563,28 +565,113 @@ namespace StardewModdingAPI return !issuesFound; } - /// Asynchronously check for a new version of SMAPI, and print a message to the console if an update is available. - private void CheckForUpdateAsync() + /// Asynchronously check for a new version of SMAPI and any installed mods, and print alerts to the console if an update is available. + /// The mods to include in the update check (if eligible). + private void CheckForUpdatesAsync(IModMetadata[] mods) { if (!this.Settings.CheckForUpdates) return; new Thread(() => { + // update info + List updates = new List(); + bool smapiUpdate = false; + int modUpdates = 0; + + // create client + WebApiClient client = new WebApiClient(this.Settings.WebApiBaseUrl, Constants.ApiVersion); + + // fetch SMAPI version try { - var client = new WebApiClient(this.Settings.WebApiBaseUrl, Constants.ApiVersion); - string key = $"GitHub:{this.Settings.GitHubProjectName}"; - ModInfoModel info = client.GetModInfoAsync(key).Result[key]; - if (info.Error != null) - this.Monitor.Log($"Couldn't check for a new version of SMAPI. This won't affect your game, but you may not be notified of new versions if this keeps happening.\n{info.Error}"); - else if (new SemanticVersion(info.Version).IsNewerThan(Constants.ApiVersion)) - this.Monitor.Log($"You can update SMAPI from version {Constants.ApiVersion} to {info.Version}.", LogLevel.Alert); + ModInfoModel response = client.GetModInfoAsync($"GitHub:{this.Settings.GitHubProjectName}").Result.Single().Value; + if (response.Error != null) + this.Monitor.Log($"Couldn't check for a new version of SMAPI. This won't affect your game, but you may not be notified of new versions if this keeps happening.\n{response.Error}", LogLevel.Warn); + else if (new SemanticVersion(response.Version).IsNewerThan(Constants.ApiVersion)) + { + smapiUpdate = true; + updates.Add($"SMAPI {response.Version}: {response.Url}"); + } } catch (Exception ex) { this.Monitor.Log($"Couldn't check for a new version of SMAPI. This won't affect your game, but you may not be notified of new versions if this keeps happening.\n{ex.GetLogSummary()}"); } + + // fetch mod versions +#if !SMAPI_1_x + try + { + // prepare update-check data + IDictionary modsByKey = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + foreach (IModMetadata mod in mods) + { + if (!string.IsNullOrWhiteSpace(mod.Manifest.NexusID)) + modsByKey[$"Nexus:{mod.Manifest.NexusID}"] = mod; + if (!string.IsNullOrWhiteSpace(mod.Manifest.GitHubProject)) + modsByKey[$"GitHub:{mod.Manifest.GitHubProject}"] = mod; + } + + // fetch results + IDictionary response = client.GetModInfoAsync(modsByKey.Keys.ToArray()).Result; + IDictionary updatesByMod = new Dictionary(); + foreach (var entry in response) + { + // handle error + if (entry.Value.Error != null) + { + this.Monitor.Log($"Couldn't fetch version of {modsByKey[entry.Key].DisplayName} with key {entry.Key}:\n{entry.Value.Error}", LogLevel.Trace); + continue; + } + + // collect latest mod version + IModMetadata mod = modsByKey[entry.Key]; + ISemanticVersion version = new SemanticVersion(entry.Value.Version); + if (version.IsNewerThan(mod.Manifest.Version)) + { + if (!updatesByMod.TryGetValue(mod, out ModInfoModel other) || version.IsNewerThan(other.Version)) + { + updatesByMod[mod] = entry.Value; + modUpdates++; + } + } + } + + // add to output queue + if (updatesByMod.Any()) + { + foreach (var entry in updatesByMod.OrderBy(p => p.Key.DisplayName)) + updates.Add($"{entry.Key.DisplayName} {entry.Value.Version}: {entry.Value.Url}"); + } + } + catch (Exception ex) + { + this.Monitor.Log($"Couldn't check for new mod versions:\n{ex.GetLogSummary()}", LogLevel.Trace); + } +#endif + + // output + if (updates.Any()) + { +#if !SMAPI_1_x + this.Monitor.Newline(); +#endif + + // print intro + string intro = ""; + if (smapiUpdate) + intro = "You can update SMAPI"; + if (modUpdates > 0) + intro += $"{(smapiUpdate ? " and" : "You can update")} {modUpdates} mod{(modUpdates != 1 ? "s" : "")}"; + intro += ":"; + this.Monitor.Log(intro, LogLevel.Alert); + + // print update list + foreach (string line in updates) + this.Monitor.Log($" {line}", LogLevel.Alert); + } + }).Start(); } diff --git a/src/StardewModdingAPI/StardewModdingAPI.config.json b/src/StardewModdingAPI/StardewModdingAPI.config.json index 67d8f270..c91d169c 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.config.json +++ b/src/StardewModdingAPI/StardewModdingAPI.config.json @@ -15,9 +15,9 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "DeveloperMode": true, /** - * Whether SMAPI should check for a newer version when you load the game. If a new version is - * available, a small message will appear in the console. This doesn't affect the load time even - * if your connection is offline or slow, because it happens in the background. + * Whether SMAPI should check for newer versions of SMAPI and mods when you load the game. If new + * versions are available, an alert will be shown in the console. This doesn't affect the load + * time even if your connection is offline or slow, because it happens in the background. */ "CheckForUpdates": true, From 57111a6e8fa6a23bb56f515b50f8b7ea5924d49f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 23 Sep 2017 20:03:45 -0400 Subject: [PATCH 092/186] update file name (#336) --- .../Framework/{UpdateHelper.cs => WebApiClient.cs} | 0 src/StardewModdingAPI/StardewModdingAPI.csproj | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/StardewModdingAPI/Framework/{UpdateHelper.cs => WebApiClient.cs} (100%) diff --git a/src/StardewModdingAPI/Framework/UpdateHelper.cs b/src/StardewModdingAPI/Framework/WebApiClient.cs similarity index 100% rename from src/StardewModdingAPI/Framework/UpdateHelper.cs rename to src/StardewModdingAPI/Framework/WebApiClient.cs diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index d5fffd3f..07e98674 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -218,7 +218,7 @@ - + From a149f82b7a00d1ebf5ab33e529be93ce70873947 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 6 Sep 2017 22:04:21 -0400 Subject: [PATCH 093/186] update compatibility list for SMAPI 2.0 --- .../StardewModdingAPI.config.json | 602 ++++++++++++++++-- 1 file changed, 553 insertions(+), 49 deletions(-) diff --git a/src/StardewModdingAPI/StardewModdingAPI.config.json b/src/StardewModdingAPI/StardewModdingAPI.config.json index c91d169c..e5c42391 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.config.json +++ b/src/StardewModdingAPI/StardewModdingAPI.config.json @@ -93,7 +93,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": [ "1e36d4ca-c7ef-4dfb-9927-d27a6c3c8bdc" ], "UpperVersion": "0.0", "UpperVersionLabel": "0.01", - "UpdateUrls": [ "http://community.playstarbound.com/resources/3532", "http://community.playstarbound.com/threads/132096" ], + "UpdateUrls": [ "http://community.playstarbound.com/resources/3532", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 1.9." }, { @@ -101,7 +101,14 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": [ "Entoarox.AdvancedLocationLoader" ], "UpperVersion": "1.2.10", "UpdateUrls": [ "http://community.playstarbound.com/resources/3619" ], - "Notes": "Overhauled for SMAPI 1.11+ compatibility." + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "AgingMod", + "ID": [ "skn.AgingMod" ], + "UpperVersion": "1.0", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1129", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." }, { "Name": "Almighty Tool", @@ -110,25 +117,67 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/439" ], "Notes": "Broke in SDV 1.2." }, + { + "Name": "Animal Sitter", + "ID": [ "AnimalSitter.dll" ], + "UpperVersion": "1.0.8", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/581", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "A Tapper's Dream", + "ID": [ "ddde5195-8f85-4061-90cc-0d4fd5459358" ], + "UpperVersion": "1.4", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/260", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, { "Name": "Better Sprinklers", "ID": [ "SPDSprinklersMod", /*since 2.3*/ "Speeder.BetterSprinklers" ], "UpperVersion": "2.3.1-pathoschild-update", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/41", "http://community.playstarbound.com/threads/132096" ], + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/41", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SDV 1.2." }, { "Name": "Birthday Mail", "ID": [ "005e02dc-d900-425c-9c68-1ff55c5a295d" ], "UpperVersion": "1.2.2", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/276", "http://community.playstarbound.com/threads/132096" ], + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/276", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SDV 1.2." }, + { + "Name": "Build Endurance", + "ID": [ "{'ID':'4be88c18-b6f3-49b0-ba96-f94b1a5be890', 'Name':'BuildEndurance'}" ], // disambiguate from other Alpha_Omegasis mods + "UpperVersion": "1.3", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/445", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "Build Health", + "ID": [ "{'ID':'4be88c18-b6f3-49b0-ba96-f94b1a5be890', 'Name':'BuildHealth'}" ], // disambiguate from other Alpha_Omegasis mods + "UpperVersion": "1.3", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/446", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "Buy Cooking Recipes", + "ID": [ "Denifia.BuyRecipes" ], + "UpperVersion": "1.0", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1126", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "Buy Back Collectables", + "ID": [ "BuyBackCollectables" ], + "UpperVersion": "1.3", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/507", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, { "Name": "Chest Label System", "ID": [ "SPDChestLabel" ], "UpperVersion": "1.6", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/242", "http://community.playstarbound.com/threads/132096" ], + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/242", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SDV 1.1." }, { @@ -145,6 +194,13 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/518" ], "Notes": "Broke in SDV 1.2." }, + { + "Name": "Choose Baby Gender", + "ID": [ "ChooseBabyGender.dll" ], + "UpperVersion": "1.0.2", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/590", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, { "Name": "CJB Automation", "ID": [ "CJBAutomation" ], @@ -173,54 +229,186 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/93" ], "Notes": "Broke in SDV 1.2." }, + { + "Name": "Climates of Ferngill", + "ID": [ "KoihimeNakamura.ClimatesOfFerngill" ], + "UpperVersion": "1.0", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/604", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "Cold Weather Haley", + "ID": [ "LordXamon.ColdWeatherHaleyPRO" ], + "UpperVersion": "1.0", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1169", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "Combat with Farm Implements", + "ID": [ "SPDFarmingImplementsInCombat" ], + "UpperVersion": "1.0", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/313", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "Configurable Shipping Dates", + "ID": [ "ConfigurableShippingDates" ], + "UpperVersion": "1.1.1", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/675", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, { "Name": "Cooking Skill", - "ID": [ "CookingSkill" ], - "UpperVersion": "1.0.3", + "ID": [ "CookingSkill", /*since 1.0.4–6*/ "spacechase0.CookingSkill" ], + "UpperVersion": "1.0.6", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/522" ], - "Notes": "Broke in SDV 1.2." + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "CrabNet", + "ID": [ "CrabNet.dll" ], + "UpperVersion": "1.0.4", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/584", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "Customize Exterior", + "ID": [ "CustomizeExterior" ], + "UpperVersion": "1.0.2", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1099" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "Customizable Traveling Cart Days", + "ID": [ "TravelingCartYyeahdude" ], + "UpperVersion": "1.0", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/567", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "Daily News", + "ID": [ "bashNinja.DailyNews" ], + "UpperVersion": "1.1", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1141", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "Dynamic Checklist", + "ID": [ "gunnargolf.DynamicChecklist" ], + "UpperVersion": "1.0", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1145", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "Dynamic Machines", + "ID": [ "DynamicMachines" ], + "UpperVersion": "1.1", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/374", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "Empty Hands", + "ID": [ "QuicksilverFox.EmptyHands" ], + "UpperVersion": "1.0", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1176", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." }, { "Name": "Enemy Health Bars", "ID": [ "SPDHealthBar" ], "UpperVersion": "1.7", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/193", "http://community.playstarbound.com/threads/132096" ], + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/193", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SDV 1.2." }, { "Name": "Entoarox Framework", "ID": [ "eacdb74b-4080-4452-b16b-93773cda5cf9", /*since ???*/ "Entoarox.EntoaroxFramework" ], - "UpperVersion": "1.7.10", + "UpperVersion": "1.7.9", "UpdateUrls": [ "http://community.playstarbound.com/resources/4228" ], - "Notes": "Overhauled for SMAPI 1.11+ compatibility." + "Notes": "Broke in SMAPI 2.0." }, { "Name": "Extended Fridge", "ID": [ "Mystra007ExtendedFridge" ], "UpperVersion": "1.0", - "UpperVersionLabel": "0.94", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/485" ], - "Notes": "Broke in SDV 1.2. Actual upper version is 0.94, but mod incorrectly sets it to 1.0 in the manifest." + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/485", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SDV 1.2." + }, + { + "Name": "Extended Greenhouse", + "ID": [ "ExtendedGreenhouse" ], + "UpperVersion": "1.0.2", + "UpdateUrls": [ "http://community.playstarbound.com/resources/4303", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SDV 1.2." + }, + { + "Name": "Fall 28 Snow Day", + "ID": [ "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Fall28 Snow Day'}" ], // disambiguate from other mods by Alpha_Omegasis + "UpperVersion": "1.3", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/486", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "Farm Automation: Barn Door Automation", + "ID": [ "FarmAutomation.BarnDoorAutomation.dll" ], + "UpperVersion": "1.0", + "UpdateUrls": [ "http://community.playstarbound.com/threads/111931", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." }, { "Name": "Farm Automation: Item Collector", "ID": [ "FarmAutomation.ItemCollector.dll" ], "UpperVersion": "1.0", - "UpdateUrls": [ "http://community.playstarbound.com/threads/111931", "http://community.playstarbound.com/threads/125172" ], + "UpdateUrls": [ "http://community.playstarbound.com/threads/111931", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SDV 1.2." }, { "Name": "Farm Automation Unofficial: Item Collector", "ID": [ "Maddy99.FarmAutomation.ItemCollector" ], - "UpperVersion": "0.4", - "UpdateUrls": [ "http://community.playstarbound.com/threads/125172" ], - "Notes": "Broke in SDV 1.2." + "UpperVersion": "0.5", + "UpdateUrls": [ "http://community.playstarbound.com/threads/125172", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "Farm Expansion", + "ID": [ "3888bdfd-73f6-4776-8bb7-8ad45aea1915", /*since 2.0*/ "AdvizeFarmExpansionMod-2-0", /*since 2.0.5*/ "AdvizeFarmExpansionMod-2-0-5" ], + "UpperVersion": "2.0.5", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/130", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "Farm Resource Generator", + "ID": [ "FarmResourceGenerator.dll" ], + "UpperVersion": "1.0.4", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/647", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "Faster Run", + "ID": [ "FasterRun.dll" ], + "UpperVersion": "1.1", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/733", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "FlorenceMod", + "ID": [ "FlorenceMod.dll" ], + "UpperVersion": "1.1", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/591", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "Forage at the Farm", + "ID": [ "ForageAtTheFarm" ], + "UpperVersion": "1.5.1", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/673", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." }, { "Name": "Instant Geode", "ID": [ "InstantGeode" ], "UpperVersion": "1.12", - "UpdateUrls": [ "http://community.playstarbound.com/threads/109038", "http://community.playstarbound.com/threads/132096" ], + "UpdateUrls": [ "http://community.playstarbound.com/threads/109038", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SDV 1.2." }, { @@ -230,6 +418,13 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "UpdateUrls": [ "http://community.playstarbound.com/threads/111988" ], "Notes": "Broke in SDV 1.2." }, + { + "Name": "GenericShopExtender", + "ID": [ "GenericShopExtender" ], + "UpperVersion": "0.1.2", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/814", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, { "Name": "Get Dressed", "ID": [ "GetDressed.dll", /*since 3.3*/ "Advize.GetDressed" ], @@ -244,6 +439,48 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/229" ], "Notes": "Broke in SDV 1.2." }, + { + "Name": "Happy Animals", + "ID": [ "HappyAnimals" ], + "UpperVersion": "1.0.3", + "UpdateUrls": [ "https://community.playstarbound.com/threads/126655", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "Happy Birthday", + "ID": [ "{ID:'HappyBirthday', Author:'Alpha_Omegasis'}" ], // disambiguate from Oxyligen's fork + "UpperVersion": "1.3", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/520", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "Harvest With Scythe", + "ID": [ "965169fd-e1ed-47d0-9f12-b104535fb4bc" ], + "UpperVersion": "1.0.6", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/236", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "Hunger for Food", + "ID": [ "HungerForFoodByTigerle" ], + "UpperVersion": "0.1.2", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/810", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "Improved Quality of Life", + "ID": [ "Demiacle.ImprovedQualityOfLife" ], + "UpperVersion": "1.1", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1025", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "Less Strict Over-Exertion (AntiExhaustion)", + "ID": [ "BALANCEMOD_AntiExhaustion" ], + "UpperVersion": "1.1", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/637", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, { "Name": "Lookup Anything", "ID": [ "LookupAnything", /*since 1.10.1*/ "Pathoschild.LookupAnything" ], @@ -251,12 +488,47 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/541" ], "Notes": "Broke in SDV 1.2." }, + { + "Name": "Loved Labels", + "ID": [ "LovedLabels.dll" ], + "UpperVersion": "2.0", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/279" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "Luck Skill", + "ID": [ "LuckSkill", /*since 0.1.4*/ "spacechase0.LuckSkill" ], + "UpperVersion": "0.1.4", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/521" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "MailOrderPigs", + "ID": [ "MailOrderPigs.dll" ], + "UpperVersion": "1.0.1", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/632", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, { "Name": "Makeshift Multiplayer", "ID": [ "StardewValleyMP", /*since 0.3*/ "spacechase0.StardewValleyMP" ], - "UpperVersion": "0.3.3", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/501" ], - "Notes": "Broke in SDV 1.2." + "UpperVersion": "0.3.6", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/501", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "Message Box [API]? (ChatMod)", + "ID": [ "Kithio:ChatMod" ], + "UpperVersion": "1.0", + "UpdateUrls": [ "http://community.playstarbound.com/resources/message-box-api.4296", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "More Artifact Spots", + "ID": [ "451" ], + "UpperVersion": "1.0.1", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/451", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." }, { "Name": "More Pets", @@ -265,11 +537,46 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "UpdateUrls": [ "http://community.playstarbound.com/resources/4288" ], "Notes": "Overhauled for SMAPI 1.11+ compatibility." }, + { + "Name": "More Rain", + "ID": [ "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'More_Rain'}" ], // disambiguate from other mods by Alpha_Omegasis + "UpperVersion": "1.4", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/441", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "Multiple Sprites and Portraits On Rotation (File Loading)", + "ID": [ "FileLoading" ], + "UpperVersion": "1.12", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1094", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "Museum Rearranger", + "ID": [ "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Museum Rearranger'}" ], // disambiguate from other mods by Alpha_Omegasis + "UpperVersion": "1.3", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/428", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "New Machines", + "ID": [ "F70D4FAB-0AB2-4B78-9F1B-AF2CA2236A59" ], + "UpperVersion": "4.2.1343", + "UpdateUrls": [ "http://community.playstarbound.com/resources/3683", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "Night Owl", + "ID": [ "{'ID':'SaveAnywhere', 'Name':'Stardew_NightOwl'}" ], // disambiguate from Save Anywhere + "UpperVersion": "2.1", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/433", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, { "Name": "NoSoilDecay", "ID": [ "289dee03-5f38-4d8e-8ffc-e440198e8610" ], "UpperVersion": "0.5", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/237", "http://community.playstarbound.com/threads/132096" ], + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/237", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SDV 1.2, and uses Assembly.GetExecutingAssembly().Location." }, { @@ -280,34 +587,97 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/239" ], "ReasonPhrase": "this version has an update check error which crashes the game." }, + { + "Name": "NPC Speak", + "ID": [ "NpcEcho.dll" ], + "UpperVersion": "1.0", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/694", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0" + }, + { + "Name": "OmniFarm", + "ID": [ "BlueMod_OmniFarm" ], + "UpperVersion": "2.0.1", + "UpdateUrls": [ "http://community.playstarbound.com/threads/127299", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0" + }, { "Name": "Part of the Community", "ID": [ "SB_PotC" ], "UpperVersion": "1.0.8", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/923" ], - "ReasonPhrase": "Broke in SDV 1.2." + "Notes": "Broke in SDV 1.2." + }, + { + "Name": "PelicanFiber", + "ID": [ "PelicanFiber.dll" ], + "UpperVersion": "3.0", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/631", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "PelicanTTS", + "ID": [ "Platonymous.PelicanTTS" ], + "UpperVersion": "1.6", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1079", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." }, { "Name": "Persival's BundleMod", "ID": [ "BundleMod.dll" ], "UpperVersion": "1.0", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/438", "http://community.playstarbound.com/threads/132096" ], + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/438", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SDV 1.1." }, { "Name": "Point-and-Plant", "ID": [ "PointAndPlant.dll" ], "UpperVersion": "1.0.2", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/572" ], + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/572", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SDV 1.2." }, { "Name": "PrairieKingMadeEasy", "ID": [ "PrairieKingMadeEasy.dll" ], "UpperVersion": "1.0", - "UpdateUrls": [ "http://community.playstarbound.com/resources/3594", "http://community.playstarbound.com/threads/132096" ], + "UpdateUrls": [ "http://community.playstarbound.com/resources/3594", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SDV 1.2." }, + { + "Name": "RainRandomizer", + "ID": [ "RainRandomizer.dll" ], + "UpperVersion": "1.0.3", + "UpdateUrls": [ "http://community.playstarbound.com/threads/111526", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "RelationshipsEnhanced", + "ID": [ "relationshipsenhanced" ], + "UpperVersion": "1.0", + "UpdateUrls": [ "http://community.playstarbound.com/resources/4435", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "RelationShipStatus", + "ID": [ "relationshipstatus" ], + "UpperVersion": "1.0.5", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/751", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "Replanter", + "ID": [ "Replanter.dll" ], + "UpperVersion": "1.0.4", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/589", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "Reusable Wallpapers and Floors (Wallpaper Retain)", + "ID": [ "dae1b553-2e39-43e7-8400-c7c5c836134b" ], + "UpperVersion": "1.5", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/356", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, { "Name": "Rush Orders", "ID": [ "RushOrders", /*since 1.1*/ "spacechase0.RushOrders" ], @@ -318,50 +688,120 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Save Anywhere", "ID": [ "{'ID':'SaveAnywhere', 'Name':'Save Anywhere'}" ], // disambiguate from Night Owl - "UpperVersion": "2.3", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/444" ], - "Notes": "Broke in SDV 1.2." + "UpperVersion": "2.4", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/444", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." }, { "Name": "Seasonal Immersion", "ID": [ "EntoaroxSeasonalHouse", /*since 1.1.0*/ "EntoaroxSeasonalBuildings", /* since 1.6 or earlier*/ "EntoaroxSeasonalImmersion", /*since 1.7*/ "Entoarox.SeasonalImmersion" ], "UpperVersion": "1.8.2", - "UpdateUrls": [ "http://community.playstarbound.com/resources/4262" ], - "Notes": "Overhauled for SMAPI 1.11+ compatibility." + "UpdateUrls": [ "http://community.playstarbound.com/resources/4262", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "Send Items", + "ID": [ "Denifia.SendItems" ], + "UpperVersion": "1.0.2", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1087", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "Shed Notifications (BuildingsNotifications)", + "ID": [ "TheCroak.BuildingsNotifications" ], + "UpperVersion": "0.4.1", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/620", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "Shenandoah Project", + "ID": [ "Shenandoah Project" ], + "UpperVersion": "1.1.1", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/756", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "Shipment Tracker", + "ID": [ "7e474181-e1a0-40f9-9c11-d08a3dcefaf3" ], + "UpperVersion": "1.1", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/321", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." }, { "Name": "Shop Expander", "ID": [ /*since at least 1.4*/ "821ce8f6-e629-41ad-9fde-03b54f68b0b6", /*since 1.5*/ "EntoaroxShopExpander", /*since 1.5.2*/ "Entoarox.ShopExpander" ], "UpperVersion": "1.5.3", - "UpdateUrls": [ "http://community.playstarbound.com/resources/4381" ], - "Notes": "Overhauled for SMAPI 1.11+ compatibility." + "UpdateUrls": [ "http://community.playstarbound.com/resources/4381", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "Showcase Mod", + "ID": [ "Igorious.Showcase" ], + "UpperVersion": "0.9", + "UpdateUrls": [ "http://community.playstarbound.com/resources/4487", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." }, { "Name": "Simple Sprinklers", "ID": [ "SimpleSprinkler.dll" ], "UpperVersion": "1.4", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/76" ], + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/76", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SDV 1.2." }, { "Name": "Siv's Marriage Mod", "ID": [ "6266959802" ], "UpperVersion": "1.2.2", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/366", "http://community.playstarbound.com/threads/132096" ], + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/366", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 1.9 (has multiple Mod instances)." }, + { + "Name": "Skill Prestige", + "ID": [ "6b843e60-c8fc-4a25-a67b-4a38ac8dcf9b" ], + "UpperVersion": "1.0.9", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/569", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, { "Name": "Skill Prestige: Cooking Adapter", "ID": [ "20d6b8a3-b6e7-460b-a6e4-07c2b0cb6c63" ], - "UpperVersion": "1.0.4", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/569", "http://community.playstarbound.com/threads/132096" ], - "Notes": "Broke in SDV 1.2." + "UpperVersion": "1.0.9", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/569", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "Slower Fence Decay", + "ID": [ "SPDSlowFenceDecay" ], + "UpperVersion": "0.5.1", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/252", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "Smart Mod", + "ID": [ "KuroBear.SmartMod" ], + "UpperVersion": "2.2", + "UpdateUrls": [ "http://community.playstarbound.com/threads/108104", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "Solar Eclipse Event", + "ID": [ "KoihimeNakamura.SolarEclipseEvent" ], + "UpperVersion": "1.1", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/897", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "Sprinkles", + "ID": [ "Platonymous.Sprinkles" ], + "UpperVersion": "1.1.3", + "UpdateUrls": [ "http://community.playstarbound.com/resources/4592", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." }, { "Name": "Sprint and Dash", "ID": [ "SPDSprintAndDash" ], "UpperVersion": "1.0", - "UpdateUrls": [ "http://community.playstarbound.com/resources/3531", "http://community.playstarbound.com/resources/4201" ], + "UpdateUrls": [ "http://community.playstarbound.com/resources/3531", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SDV 1.2." }, { @@ -375,7 +815,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Name": "Sprinting Mod", "ID": [ "a10d3097-b073-4185-98ba-76b586cba00c" ], "UpperVersion": "2.1", - "UpdateUrls": [ "http://community.playstarbound.com/threads/108313", "http://community.playstarbound.com/threads/132096" ], + "UpdateUrls": [ "http://community.playstarbound.com/threads/108313", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SDV 1.2." }, { @@ -385,25 +825,75 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/798" ], "Notes": "Broke in SDV 1.2." }, + { + "Name": "StaminaRegen", + "ID": [ "StaminaRegen.dll" ], + "UpperVersion": "1.0.3", + "UpdateUrls": [ "http://community.playstarbound.com/threads/111526", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "Stardew Auto Backup", + "ID": [ "{'ID':'4be88c18-b6f3-49b0-ba96-f94b1a5be890', 'Name':'Stardew_Save_Backup'}" ], // disambiguate from other Alpha_Omegasis mods + "UpperVersion": "1.2", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/435", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "Stardew Notification", + "ID": [ "stardewnotification" ], + "UpperVersion": "1.7", + "UpdateUrls": [ "http://community.playstarbound.com/threads/127979", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "Stardew Symphony", + "ID": [ "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'Stardew_Symphony'}" ], // disambiguate other mods by Alpha_Omegasis + "UpperVersion": "1.3", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/425", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "StashItemsToChest", + "ID": [ "BlueMod_StashItemsToChest" ], + "UpperVersion": "1.0.1", + "UpdateUrls": [ "http://community.playstarbound.com/threads/126906", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "Stone Bridge Over Pond (PondWithBridge)", + "ID": [ "PondWithBridge.dll" ], + "UpperVersion": "1.0", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/316", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "Super Greenhouse Warp Modifier", + "ID": [ "SuperGreenhouse" ], + "UpperVersion": "1.0", + "UpperVersionLabel": "1.0 (2016-11-29)", + "UpdateUrls": [ "http://community.playstarbound.com/resources/4334", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, { "Name": "Tainted Cellar", "ID": [ "TaintedCellar.dll" ], "UpperVersion": "1.0", - "UpdateUrls": [ "http://community.playstarbound.com/threads/115735", "http://community.playstarbound.com/threads/132096" ], + "UpdateUrls": [ "http://community.playstarbound.com/threads/115735", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SDV 1.1 or 1.11." }, { "Name": "Teleporter", "ID": [ "Teleporter" ], "UpperVersion": "1.0.2", - "UpdateUrls": [ "http://community.playstarbound.com/resources/4374" ], + "UpdateUrls": [ "http://community.playstarbound.com/resources/4374", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SDV 1.2." }, { "Name": "Three-heart Dance Partner", "ID": [ "ThreeHeartDancePartner" ], "UpperVersion": "1.0.1", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/500", "http://community.playstarbound.com/threads/132096" ], + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/500", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SDV 1.2." }, { @@ -417,29 +907,43 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Name": "UiModSuite", "ID": [ "Demiacle.UiModSuite" ], "UpperVersion": "1.0", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1023" ], + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1023", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SDV 1.2." }, + { + "Name": "WakeUp", + "ID": [ "WakeUp.dll" ], + "UpperVersion": "1.0.2", + "UpdateUrls": [ "http://community.playstarbound.com/threads/111526", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, + { + "Name": "Wallpaper Fix", + "ID": [ "WallpaperFix.dll" ], + "UpperVersion": "1.1", + "UpdateUrls": [ "http://community.playstarbound.com/resources/4211", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." + }, { "Name": "Weather Controller", "ID": [ "WeatherController.dll" ], "UpperVersion": "1.0.2", - "UpdateUrls": [ "http://community.playstarbound.com/threads/111526", "http://community.playstarbound.com/threads/132096" ], + "UpdateUrls": [ "http://community.playstarbound.com/threads/111526", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SDV 1.2." }, { "Name": "Wonderful Farm Life", "ID": [ "WonderfulFarmLife.dll" ], "UpperVersion": "1.0", - "UpdateUrls": [ "http://community.playstarbound.com/threads/115384", "http://community.playstarbound.com/threads/132096" ], + "UpdateUrls": [ "http://community.playstarbound.com/threads/115384", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SDV 1.1 or 1.11." }, { "Name": "Xnb Loader", "ID": [ "Entoarox.XnbLoader" ], "UpperVersion": "1.0.6", - "UpdateUrls": [ "http://community.playstarbound.com/resources/4506" ], - "Notes": "Overhauled for SMAPI 1.11+ compatibility." + "UpdateUrls": [ "http://community.playstarbound.com/resources/4506", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Notes": "Broke in SMAPI 2.0." }, { "Name": "zDailyIncrease", From 2d36105c33ffba77eb979ef6ef0d2e7d906b09bc Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 23 Sep 2017 20:53:12 -0400 Subject: [PATCH 094/186] drop support for SMAPI 1.x (#360) --- README.md | 12 +- .../InteractiveInstaller.cs | 6 - .../Core/ModResolverTests.cs | 6 +- .../Core/TranslationTests.cs | 3 +- src/StardewModdingAPI/Command.cs | 159 --------- src/StardewModdingAPI/Config.cs | 188 ---------- src/StardewModdingAPI/Constants.cs | 7 +- .../Events/EventArgsCommand.cs | 28 -- .../Events/EventArgsFarmerChanged.cs | 33 -- .../Events/EventArgsInput.cs | 2 - .../Events/EventArgsLoadedGameChanged.cs | 27 -- .../Events/EventArgsNewDay.cs | 37 -- .../Events/EventArgsStringChanged.cs | 31 -- src/StardewModdingAPI/Events/GameEvents.cs | 112 ------ src/StardewModdingAPI/Events/InputEvents.cs | 2 - src/StardewModdingAPI/Events/PlayerEvents.cs | 74 +--- src/StardewModdingAPI/Events/TimeEvents.cs | 128 +------ .../Framework/CursorPosition.cs | 2 - .../Framework/ModHelpers/ReflectionHelper.cs | 4 +- .../Framework/ModLoading/ModResolver.cs | 25 +- .../Framework/ModRegistry.cs | 8 +- .../Framework/Models/Manifest.cs | 9 - .../Framework/Models/ManifestDependency.cs | 12 +- src/StardewModdingAPI/Framework/Monitor.cs | 27 +- src/StardewModdingAPI/Framework/SGame.cs | 81 +---- .../Serialisation/SFieldConverter.cs | 6 +- .../Framework/Utilities/ContextHash.cs | 3 +- src/StardewModdingAPI/IContentHelper.cs | 6 +- src/StardewModdingAPI/ICursorPosition.cs | 2 - src/StardewModdingAPI/IManifestDependency.cs | 4 +- src/StardewModdingAPI/ISemanticVersion.cs | 9 +- src/StardewModdingAPI/Log.cs | 320 ------------------ .../Metadata/InstructionMetadata.cs | 2 - src/StardewModdingAPI/Mod.cs | 90 +---- src/StardewModdingAPI/Program.cs | 138 +------- src/StardewModdingAPI/SemanticVersion.cs | 4 +- .../StardewModdingAPI.csproj | 8 - src/StardewModdingAPI/Utilities/SButton.cs | 14 +- src/StardewModdingAPI/Utilities/SDate.cs | 4 - .../Framework/Commands/Saves/LoadCommand.cs | 30 -- .../Framework/Commands/Saves/SaveCommand.cs | 29 -- src/TrainerMod/TrainerMod.csproj | 2 - 42 files changed, 37 insertions(+), 1657 deletions(-) delete mode 100644 src/StardewModdingAPI/Command.cs delete mode 100644 src/StardewModdingAPI/Config.cs delete mode 100644 src/StardewModdingAPI/Events/EventArgsCommand.cs delete mode 100644 src/StardewModdingAPI/Events/EventArgsFarmerChanged.cs delete mode 100644 src/StardewModdingAPI/Events/EventArgsLoadedGameChanged.cs delete mode 100644 src/StardewModdingAPI/Events/EventArgsNewDay.cs delete mode 100644 src/StardewModdingAPI/Events/EventArgsStringChanged.cs delete mode 100644 src/StardewModdingAPI/Log.cs delete mode 100644 src/TrainerMod/Framework/Commands/Saves/LoadCommand.cs delete mode 100644 src/TrainerMod/Framework/Commands/Saves/SaveCommand.cs diff --git a/README.md b/README.md index ebc2dd57..7a5f9009 100644 --- a/README.md +++ b/README.md @@ -84,13 +84,13 @@ on the wiki for the first-time setup. build type | format | example :--------- | :-------------------------------- | :------ - dev build | `-alpha.` | `1.0-alpha.20171230` - prerelease | `-prerelease.` | `1.0-prerelease.2` - release | `` | `1.0` + dev build | `-alpha.` | `2.0-alpha.20171230` + prerelease | `-prerelease.` | `2.0-prerelease.2` + release | `` | `2.0` 2. In Windows: 1. Rebuild the solution in _Release_ mode. - 2. Rename `bin/Packaged` to `SMAPI ` (e.g. `SMAPI 1.0`). + 2. Rename `bin/Packaged` to `SMAPI ` (e.g. `SMAPI 2.0`). 2. Transfer the `SMAPI ` folder to Linux or Mac. _This adds the installer executable and Windows files. We'll do the rest in Linux or Mac, since we need to set Unix file permissions that Windows won't save._ @@ -101,7 +101,7 @@ on the wiki for the first-time setup. 3. If you did everything right so far, you should have a folder like this: ``` - SMAPI-1.x/ + SMAPI-2.x/ install.exe readme.txt internal/ @@ -181,4 +181,4 @@ SMAPI uses a small number of conditional compilation constants, which you can se flag | purpose ---- | ------- `SMAPI_FOR_WINDOWS` | Indicates that SMAPI is being compiled on Windows for players on Windows. Set automatically in `crossplatform.targets`. -`SMAPI_1_x` | Sets legacy SMAPI 1._x_ mode, disables SMAPI 2.0 features, and enables deprecated code. This will be removed when SMAPI 2.0 is released. + diff --git a/src/StardewModdingAPI.Installer/InteractiveInstaller.cs b/src/StardewModdingAPI.Installer/InteractiveInstaller.cs index b52529a6..83638e10 100644 --- a/src/StardewModdingAPI.Installer/InteractiveInstaller.cs +++ b/src/StardewModdingAPI.Installer/InteractiveInstaller.cs @@ -135,11 +135,6 @@ namespace StardewModdingApi.Installer /// public void Run(string[] args) { -#if SMAPI_1_x - bool installArg = false; - bool uninstallArg = false; - string gamePathArg = null; -#else /**** ** read command-line arguments ****/ @@ -160,7 +155,6 @@ namespace StardewModdingApi.Installer if (pathIndex >= 1 && args.Length >= pathIndex) gamePathArg = args[pathIndex]; } -#endif /**** ** collect details diff --git a/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs b/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs index fc84ca29..7a81c68c 100644 --- a/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs +++ b/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs @@ -141,7 +141,7 @@ namespace StardewModdingAPI.Tests.Core { // arrange Mock mock = this.GetMetadata("Mod A", new string[0], allowStatusChange: true); - this.SetupMetadataForValidation(mock, new ModCompatibility { Compatibility = ModCompatibilityType.AssumeBroken, UpperVersion = new SemanticVersion("1.0"), UpdateUrls = new[] { "http://example.org" }}); + this.SetupMetadataForValidation(mock, new ModCompatibility { Compatibility = ModCompatibilityType.AssumeBroken, UpperVersion = new SemanticVersion("1.0"), UpdateUrls = new[] { "http://example.org" } }); // act new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0")); @@ -179,7 +179,6 @@ namespace StardewModdingAPI.Tests.Core mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "The validation did not fail the metadata."); } -#if !SMAPI_1_x [Test(Description = "Assert that validation fails when multiple mods have the same unique ID.")] public void ValidateManifests_DuplicateUniqueID_Fails() { @@ -197,7 +196,6 @@ namespace StardewModdingAPI.Tests.Core modA.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "The validation did not fail the first mod with a unique ID."); modB.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "The validation did not fail the second mod with a unique ID."); } -#endif [Test(Description = "Assert that validation fails when the manifest references a DLL that does not exist.")] public void ValidateManifests_Valid_Passes() @@ -423,7 +421,6 @@ namespace StardewModdingAPI.Tests.Core Assert.AreSame(modB.Object, mods[1], "The load order is incorrect: mod B should be second since it needs mod A."); } -#if !SMAPI_1_x [Test(Description = "Assert that optional dependencies are sorted correctly if present.")] public void ProcessDependencies_IfOptional() { @@ -455,7 +452,6 @@ namespace StardewModdingAPI.Tests.Core Assert.AreEqual(1, mods.Length, 0, "Expected to get the same number of mods input."); Assert.AreSame(modB.Object, mods[0], "The load order is incorrect: mod B should be first since it's the only mod."); } -#endif /********* diff --git a/src/StardewModdingAPI.Tests/Core/TranslationTests.cs b/src/StardewModdingAPI.Tests/Core/TranslationTests.cs index 8511e765..63404a41 100644 --- a/src/StardewModdingAPI.Tests/Core/TranslationTests.cs +++ b/src/StardewModdingAPI.Tests/Core/TranslationTests.cs @@ -1,8 +1,7 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; -using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.ModHelpers; using StardewValley; diff --git a/src/StardewModdingAPI/Command.cs b/src/StardewModdingAPI/Command.cs deleted file mode 100644 index 76c85287..00000000 --- a/src/StardewModdingAPI/Command.cs +++ /dev/null @@ -1,159 +0,0 @@ -#if SMAPI_1_x -using System; -using System.Collections.Generic; -using StardewModdingAPI.Events; -using StardewModdingAPI.Framework; - -namespace StardewModdingAPI -{ - /// A command that can be submitted through the SMAPI console to interact with SMAPI. - [Obsolete("Use " + nameof(IModHelper) + "." + nameof(IModHelper.ConsoleCommands))] - public class Command - { - /********* - ** Properties - *********/ - /// The commands registered with SMAPI. - private static readonly IDictionary LegacyCommands = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - - /// Manages console commands. - private static CommandManager CommandManager; - - /// Manages deprecation warnings. - private static DeprecationManager DeprecationManager; - - /// Tracks the installed mods. - private static ModRegistry ModRegistry; - - - /********* - ** Accessors - *********/ - /// The event raised when this command is submitted through the console. - public event EventHandler CommandFired; - - /**** - ** Command - ****/ - /// The name of the command. - public string CommandName; - - /// A human-readable description of what the command does. - public string CommandDesc; - - /// A human-readable list of accepted arguments. - public string[] CommandArgs; - - /// The actual submitted argument values. - public string[] CalledArgs; - - - /********* - ** Public methods - *********/ - /**** - ** Command - ****/ - /// Injects types required for backwards compatibility. - /// Manages console commands. - /// Manages deprecation warnings. - /// Tracks the installed mods. - internal static void Shim(CommandManager commandManager, DeprecationManager deprecationManager, ModRegistry modRegistry) - { - Command.CommandManager = commandManager; - Command.DeprecationManager = deprecationManager; - Command.ModRegistry = modRegistry; - } - - /// Construct an instance. - /// The name of the command. - /// A human-readable description of what the command does. - /// A human-readable list of accepted arguments. - public Command(string name, string description, string[] args = null) - { - this.CommandName = name; - this.CommandDesc = description; - if (args == null) - args = new string[0]; - this.CommandArgs = args; - } - - /// Trigger this command. - public void Fire() - { - if (this.CommandFired == null) - throw new InvalidOperationException($"Can't run command '{this.CommandName}' because it has no registered handler."); - this.CommandFired.Invoke(this, new EventArgsCommand(this)); - } - - - /**** - ** SMAPI - ****/ - /// Parse a command string and invoke it if valid. - /// The command to run, including the command name and any arguments. - /// Encapsulates monitoring and logging. - public static void CallCommand(string input, IMonitor monitor) - { - Command.DeprecationManager.Warn("Command.CallCommand", "1.9", DeprecationLevel.PendingRemoval); - Command.CommandManager.Trigger(input); - } - - /// Register a command with SMAPI. - /// The name of the command. - /// A human-readable description of what the command does. - /// A human-readable list of accepted arguments. - public static Command RegisterCommand(string name, string description, string[] args = null) - { - name = name?.Trim().ToLower(); - - // raise deprecation warning - Command.DeprecationManager.Warn("Command.RegisterCommand", "1.9", DeprecationLevel.PendingRemoval); - - // validate - if (Command.LegacyCommands.ContainsKey(name)) - throw new InvalidOperationException($"The '{name}' command is already registered!"); - - // add command - string modName = Command.ModRegistry.GetModFromStack() ?? ""; - string documentation = args?.Length > 0 - ? $"{description} - {string.Join(", ", args)}" - : description; - Command.CommandManager.Add(modName, name, documentation, Command.Fire); - - // add legacy command - Command command = new Command(name, description, args); - Command.LegacyCommands.Add(name, command); - return command; - } - - /// Find a command with the given name. - /// The command name to find. - public static Command FindCommand(string name) - { - Command.DeprecationManager.Warn("Command.FindCommand", "1.9", DeprecationLevel.PendingRemoval); - if (name == null) - return null; - - Command command; - Command.LegacyCommands.TryGetValue(name.Trim(), out command); - return command; - } - - - /********* - ** Private methods - *********/ - /// Trigger this command. - /// The command name. - /// The command arguments. - private static void Fire(string name, string[] args) - { - Command command; - if (!Command.LegacyCommands.TryGetValue(name, out command)) - throw new InvalidOperationException($"Can't run command '{name}' because there's no such legacy command."); - command.Fire(); - } - } -} -#endif \ No newline at end of file diff --git a/src/StardewModdingAPI/Config.cs b/src/StardewModdingAPI/Config.cs deleted file mode 100644 index 734c04e8..00000000 --- a/src/StardewModdingAPI/Config.cs +++ /dev/null @@ -1,188 +0,0 @@ -#if SMAPI_1_x -using System; -using System.IO; -using System.Linq; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using StardewModdingAPI.Framework; - -namespace StardewModdingAPI -{ - /// A dynamic configuration class for a mod. - [Obsolete("This base class is obsolete since SMAPI 1.0. See the latest project README for details.")] - public abstract class Config - { - /********* - ** Properties - *********/ - /// Manages deprecation warnings. - private static DeprecationManager DeprecationManager; - - - /********* - ** Accessors - *********/ - /// The full path to the configuration file. - [JsonIgnore] - public virtual string ConfigLocation { get; protected internal set; } - - /// The directory path containing the configuration file. - [JsonIgnore] - public virtual string ConfigDir => Path.GetDirectoryName(this.ConfigLocation); - - - /********* - ** Public methods - *********/ - /// Injects types required for backwards compatibility. - /// Manages deprecation warnings. - internal static void Shim(DeprecationManager deprecationManager) - { - Config.DeprecationManager = deprecationManager; - } - - /// Construct an instance of the config class. - /// The config class type. - [Obsolete("This base class is obsolete since SMAPI 1.0. See the latest project README for details.")] - public virtual Config Instance() where T : Config => Activator.CreateInstance(); - - /// Load the config from the JSON file, saving it to disk if needed. - /// The config class type. - [Obsolete("This base class is obsolete since SMAPI 1.0. See the latest project README for details.")] - public virtual T LoadConfig() where T : Config - { - // validate - if (string.IsNullOrEmpty(this.ConfigLocation)) - { - Log.Error("A config tried to load without specifying a location on the disk."); - return null; - } - - // read or generate config - T returnValue; - if (!File.Exists(this.ConfigLocation)) - { - T config = this.GenerateDefaultConfig(); - config.ConfigLocation = this.ConfigLocation; - returnValue = config; - } - else - { - try - { - T config = JsonConvert.DeserializeObject(File.ReadAllText(this.ConfigLocation)); - config.ConfigLocation = this.ConfigLocation; - returnValue = config.UpdateConfig(); - } - catch (Exception ex) - { - Log.Error($"Invalid JSON ({this.GetType().Name}): {this.ConfigLocation} \n{ex}"); - return this.GenerateDefaultConfig(); - } - } - - returnValue.WriteConfig(); - return returnValue; - } - - /// Get the default config values. - [Obsolete("This base class is obsolete since SMAPI 1.0. See the latest project README for details.")] - public virtual T GenerateDefaultConfig() where T : Config - { - return null; - } - - /// Get the current configuration with missing values defaulted. - /// The config class type. - [Obsolete("This base class is obsolete since SMAPI 1.0. See the latest project README for details.")] - public virtual T UpdateConfig() where T : Config - { - try - { - // get default + user config - JObject defaultConfig = JObject.FromObject(this.Instance().GenerateDefaultConfig()); - JObject currentConfig = JObject.FromObject(this); - defaultConfig.Merge(currentConfig, new JsonMergeSettings { MergeArrayHandling = MergeArrayHandling.Replace }); - - // cast json object to config - T config = defaultConfig.ToObject(); - - // update location - config.ConfigLocation = this.ConfigLocation; - - return config; - } - catch (Exception ex) - { - Log.Error($"An error occured when updating a config: {ex}"); - return this as T; - } - } - - - /********* - ** Protected methods - *********/ - /// Construct an instance. - protected Config() - { - Config.DeprecationManager.Warn("the Config class", "1.0", DeprecationLevel.PendingRemoval); - Config.DeprecationManager.MarkWarned($"{nameof(Mod)}.{nameof(Mod.BaseConfigPath)}", "1.0"); // typically used to construct config, avoid redundant warnings - } - } - - /// Provides extension methods for classes. - [Obsolete("This base class is obsolete since SMAPI 1.0. See the latest project README for details.")] - public static class ConfigExtensions - { - /// Initialise the configuration. That includes loading, saving, and merging the config file and in memory at a default state. This method should not be used to reload or to resave a config. NOTE: You MUST set your config EQUAL to the return of this method! - /// The config class type. - /// The base configuration to initialise. - /// The base configuration file path. - [Obsolete("This base class is obsolete since SMAPI 1.0. See the latest project README for details.")] - public static T InitializeConfig(this T baseConfig, string configLocation) where T : Config - { - if (baseConfig == null) - baseConfig = Activator.CreateInstance(); - - if (string.IsNullOrEmpty(configLocation)) - { - Log.Error("A config tried to initialize without specifying a location on the disk."); - return null; - } - - baseConfig.ConfigLocation = configLocation; - return baseConfig.LoadConfig(); - } - - /// Writes the configuration to the JSON file. - /// The config class type. - /// The base configuration to initialise. - [Obsolete("This base class is obsolete since SMAPI 1.0. See the latest project README for details.")] - public static void WriteConfig(this T baseConfig) where T : Config - { - if (string.IsNullOrEmpty(baseConfig?.ConfigLocation) || string.IsNullOrEmpty(baseConfig.ConfigDir)) - { - Log.Error("A config attempted to save when it itself or it's location were null."); - return; - } - - string json = JsonConvert.SerializeObject(baseConfig, Formatting.Indented); - if (!Directory.Exists(baseConfig.ConfigDir)) - Directory.CreateDirectory(baseConfig.ConfigDir); - - if (!File.Exists(baseConfig.ConfigLocation) || !File.ReadAllText(baseConfig.ConfigLocation).SequenceEqual(json)) - File.WriteAllText(baseConfig.ConfigLocation, json); - } - - /// Rereads the JSON file and merges its values with a default config. NOTE: You MUST set your config EQUAL to the return of this method! - /// The config class type. - /// The base configuration to initialise. - [Obsolete("This base class is obsolete since SMAPI 1.0. See the latest project README for details.")] - public static T ReloadConfig(this T baseConfig) where T : Config - { - return baseConfig.LoadConfig(); - } - } -} -#endif \ No newline at end of file diff --git a/src/StardewModdingAPI/Constants.cs b/src/StardewModdingAPI/Constants.cs index 07647d2d..fc780f40 100644 --- a/src/StardewModdingAPI/Constants.cs +++ b/src/StardewModdingAPI/Constants.cs @@ -28,12 +28,7 @@ namespace StardewModdingAPI ** Public ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } = -#if SMAPI_1_x - new SemanticVersion(1, 15, 4); -#else - new SemanticVersion(2, 0, 0, $"alpha-{DateTime.UtcNow:yyyyMMddHHmm}"); -#endif + public static ISemanticVersion ApiVersion { get; } = new SemanticVersion(2, 0, 0, $"alpha-{DateTime.UtcNow:yyyyMMddHHmm}"); /// The minimum supported version of Stardew Valley. public static ISemanticVersion MinimumGameVersion { get; } = new SemanticVersion("1.2.30"); diff --git a/src/StardewModdingAPI/Events/EventArgsCommand.cs b/src/StardewModdingAPI/Events/EventArgsCommand.cs deleted file mode 100644 index 35370139..00000000 --- a/src/StardewModdingAPI/Events/EventArgsCommand.cs +++ /dev/null @@ -1,28 +0,0 @@ -#if SMAPI_1_x -using System; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - [Obsolete("Use " + nameof(IModHelper) + "." + nameof(IModHelper.ConsoleCommands))] - public class EventArgsCommand : EventArgs - { - /********* - ** Accessors - *********/ - /// The triggered command. - public Command Command { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The triggered command. - public EventArgsCommand(Command command) - { - this.Command = command; - } - } -} -#endif \ No newline at end of file diff --git a/src/StardewModdingAPI/Events/EventArgsFarmerChanged.cs b/src/StardewModdingAPI/Events/EventArgsFarmerChanged.cs deleted file mode 100644 index 4c359939..00000000 --- a/src/StardewModdingAPI/Events/EventArgsFarmerChanged.cs +++ /dev/null @@ -1,33 +0,0 @@ -#if SMAPI_1_x -using System; -using SFarmer = StardewValley.Farmer; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class EventArgsFarmerChanged : EventArgs - { - /********* - ** Accessors - *********/ - /// The previous player character. - public SFarmer NewFarmer { get; } - - /// The new player character. - public SFarmer PriorFarmer { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The previous player character. - /// The new player character. - public EventArgsFarmerChanged(SFarmer priorFarmer, SFarmer newFarmer) - { - this.PriorFarmer = priorFarmer; - this.NewFarmer = newFarmer; - } - } -} -#endif \ No newline at end of file diff --git a/src/StardewModdingAPI/Events/EventArgsInput.cs b/src/StardewModdingAPI/Events/EventArgsInput.cs index 31368555..66cb19f2 100644 --- a/src/StardewModdingAPI/Events/EventArgsInput.cs +++ b/src/StardewModdingAPI/Events/EventArgsInput.cs @@ -1,4 +1,3 @@ -#if !SMAPI_1_x using System; using System.Linq; using Microsoft.Xna.Framework; @@ -123,4 +122,3 @@ namespace StardewModdingAPI.Events } } } -#endif diff --git a/src/StardewModdingAPI/Events/EventArgsLoadedGameChanged.cs b/src/StardewModdingAPI/Events/EventArgsLoadedGameChanged.cs deleted file mode 100644 index 688b4b3d..00000000 --- a/src/StardewModdingAPI/Events/EventArgsLoadedGameChanged.cs +++ /dev/null @@ -1,27 +0,0 @@ -#if SMAPI_1_x -using System; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class EventArgsLoadedGameChanged : EventArgs - { - /********* - ** Accessors - *********/ - /// Whether the save has been loaded. This is always true. - public bool LoadedGame { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// Whether the save has been loaded. This is always true. - public EventArgsLoadedGameChanged(bool loaded) - { - this.LoadedGame = loaded; - } - } -} -#endif \ No newline at end of file diff --git a/src/StardewModdingAPI/Events/EventArgsNewDay.cs b/src/StardewModdingAPI/Events/EventArgsNewDay.cs deleted file mode 100644 index b8cbe9e3..00000000 --- a/src/StardewModdingAPI/Events/EventArgsNewDay.cs +++ /dev/null @@ -1,37 +0,0 @@ -#if SMAPI_1_x -using System; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class EventArgsNewDay : EventArgs - { - /********* - ** Accessors - *********/ - /// The previous day value. - public int PreviousDay { get; } - - /// The current day value. - public int CurrentDay { get; } - - /// Whether the game just started the transition (true) or finished it (false). - public bool IsNewDay { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The previous day value. - /// The current day value. - /// Whether the game just started the transition (true) or finished it (false). - public EventArgsNewDay(int priorDay, int newDay, bool isTransitioning) - { - this.PreviousDay = priorDay; - this.CurrentDay = newDay; - this.IsNewDay = isTransitioning; - } - } -} -#endif \ No newline at end of file diff --git a/src/StardewModdingAPI/Events/EventArgsStringChanged.cs b/src/StardewModdingAPI/Events/EventArgsStringChanged.cs deleted file mode 100644 index f580a2ce..00000000 --- a/src/StardewModdingAPI/Events/EventArgsStringChanged.cs +++ /dev/null @@ -1,31 +0,0 @@ -#if SMAPI_1_x -using System; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a string field that changed value. - public class EventArgsStringChanged : EventArgs - { - /********* - ** Accessors - *********/ - /// The previous value. - public string NewString { get; } - - /// The current value. - public string PriorString { get; } - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The previous value. - /// The current value. - public EventArgsStringChanged(string priorString, string newString) - { - this.NewString = newString; - this.PriorString = priorString; - } - } -} -#endif \ No newline at end of file diff --git a/src/StardewModdingAPI/Events/GameEvents.cs b/src/StardewModdingAPI/Events/GameEvents.cs index deb71a86..b477376e 100644 --- a/src/StardewModdingAPI/Events/GameEvents.cs +++ b/src/StardewModdingAPI/Events/GameEvents.cs @@ -1,94 +1,17 @@ using System; -using System.Diagnostics.CodeAnalysis; using StardewModdingAPI.Framework; -#pragma warning disable 618 // Suppress obsolete-symbol errors in this file. Since several events are marked obsolete, this produces unnecessary warnings. namespace StardewModdingAPI.Events { /// Events raised when the game changes state. public static class GameEvents { - /********* - ** Properties - *********/ -#if SMAPI_1_x - /// Manages deprecation warnings. - private static DeprecationManager DeprecationManager; - - /// The backing field for . - [SuppressMessage("ReSharper", "InconsistentNaming")] - private static event EventHandler _Initialize; - - /// The backing field for . - [SuppressMessage("ReSharper", "InconsistentNaming")] - private static event EventHandler _LoadContent; - - /// The backing field for . - [SuppressMessage("ReSharper", "InconsistentNaming")] - private static event EventHandler _GameLoaded; - - /// The backing field for . - [SuppressMessage("ReSharper", "InconsistentNaming")] - private static event EventHandler _FirstUpdateTick; -#endif - - /********* ** Events *********/ /// Raised during launch after configuring XNA or MonoGame. The game window hasn't been opened by this point. Called after . internal static event EventHandler InitializeInternal; -#if SMAPI_1_x - /// Raised during launch after configuring XNA or MonoGame. The game window hasn't been opened by this point. Called after . - [Obsolete("The " + nameof(Mod) + "." + nameof(Mod.Entry) + " method is now called after the " + nameof(GameEvents.Initialize) + " event, so any contained logic can be done directly in " + nameof(Mod.Entry) + ".")] - public static event EventHandler Initialize - { - add - { - GameEvents.DeprecationManager.Warn($"{nameof(GameEvents)}.{nameof(GameEvents.Initialize)}", "1.10", DeprecationLevel.PendingRemoval); - GameEvents._Initialize += value; - } - remove => GameEvents._Initialize -= value; - } - - /// Raised before XNA loads or reloads graphics resources. Called during . - [Obsolete("The " + nameof(Mod) + "." + nameof(Mod.Entry) + " method is now called after the " + nameof(GameEvents.LoadContent) + " event, so any contained logic can be done directly in " + nameof(Mod.Entry) + ".")] - public static event EventHandler LoadContent - { - add - { - GameEvents.DeprecationManager.Warn($"{nameof(GameEvents)}.{nameof(GameEvents.LoadContent)}", "1.10", DeprecationLevel.PendingRemoval); - GameEvents._LoadContent += value; - } - remove => GameEvents._LoadContent -= value; - } - - /// Raised during launch after configuring Stardew Valley, loading it into memory, and opening the game window. The window is still blank by this point. - [Obsolete("The " + nameof(Mod) + "." + nameof(Mod.Entry) + " method is now called after the game loads, so any contained logic can be done directly in " + nameof(Mod.Entry) + ".")] - public static event EventHandler GameLoaded - { - add - { - GameEvents.DeprecationManager.Warn($"{nameof(GameEvents)}.{nameof(GameEvents.GameLoaded)}", "1.12", DeprecationLevel.PendingRemoval); - GameEvents._GameLoaded += value; - } - remove => GameEvents._GameLoaded -= value; - } - - /// Raised during the first game update tick. - [Obsolete("The " + nameof(Mod) + "." + nameof(Mod.Entry) + " method is now called after the game loads, so any contained logic can be done directly in " + nameof(Mod.Entry) + ".")] - public static event EventHandler FirstUpdateTick - { - add - { - GameEvents.DeprecationManager.Warn($"{nameof(GameEvents)}.{nameof(GameEvents.FirstUpdateTick)}", "1.12", DeprecationLevel.PendingRemoval); - GameEvents._FirstUpdateTick += value; - } - remove => GameEvents._FirstUpdateTick -= value; - } -#endif - /// Raised when the game updates its state (≈60 times per second). public static event EventHandler UpdateTick; @@ -114,48 +37,13 @@ namespace StardewModdingAPI.Events /********* ** Internal methods *********/ -#if SMAPI_1_x - /// Injects types required for backwards compatibility. - /// Manages deprecation warnings. - internal static void Shim(DeprecationManager deprecationManager) - { - GameEvents.DeprecationManager = deprecationManager; - } -#endif - /// Raise an event. /// Encapsulates logging and monitoring. internal static void InvokeInitialize(IMonitor monitor) { monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.InitializeInternal)}", GameEvents.InitializeInternal?.GetInvocationList()); -#if SMAPI_1_x - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.Initialize)}", GameEvents._Initialize?.GetInvocationList()); -#endif } -#if SMAPI_1_x - /// Raise a event. - /// Encapsulates logging and monitoring. - internal static void InvokeLoadContent(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.LoadContent)}", GameEvents._LoadContent?.GetInvocationList()); - } - - /// Raise a event. - /// Encapsulates monitoring and logging. - internal static void InvokeGameLoaded(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.GameLoaded)}", GameEvents._GameLoaded?.GetInvocationList()); - } - - /// Raise a event. - /// Encapsulates monitoring and logging. - internal static void InvokeFirstUpdateTick(IMonitor monitor) - { - monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.FirstUpdateTick)}", GameEvents._FirstUpdateTick?.GetInvocationList()); - } -#endif - /// Raise an event. /// Encapsulates logging and monitoring. internal static void InvokeUpdateTick(IMonitor monitor) diff --git a/src/StardewModdingAPI/Events/InputEvents.cs b/src/StardewModdingAPI/Events/InputEvents.cs index b99b49e0..c31eb698 100644 --- a/src/StardewModdingAPI/Events/InputEvents.cs +++ b/src/StardewModdingAPI/Events/InputEvents.cs @@ -1,4 +1,3 @@ -#if !SMAPI_1_x using System; using StardewModdingAPI.Framework; using StardewModdingAPI.Utilities; @@ -42,4 +41,3 @@ namespace StardewModdingAPI.Events } } } -#endif \ No newline at end of file diff --git a/src/StardewModdingAPI/Events/PlayerEvents.cs b/src/StardewModdingAPI/Events/PlayerEvents.cs index 72826330..5a9a9d5f 100644 --- a/src/StardewModdingAPI/Events/PlayerEvents.cs +++ b/src/StardewModdingAPI/Events/PlayerEvents.cs @@ -1,63 +1,17 @@ -using System; +using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; using StardewModdingAPI.Framework; using StardewValley; -using SFarmer = StardewValley.Farmer; -#pragma warning disable 618 // Suppress obsolete-symbol errors in this file. Since several events are marked obsolete, this produces unnecessary warnings. namespace StardewModdingAPI.Events { /// Events raised when the player data changes. public static class PlayerEvents { - /********* - ** Properties - *********/ -#if SMAPI_1_x - /// Manages deprecation warnings. - private static DeprecationManager DeprecationManager; - - /// The backing field for . - [SuppressMessage("ReSharper", "InconsistentNaming")] - private static event EventHandler _LoadedGame; - - /// The backing field for . - [SuppressMessage("ReSharper", "InconsistentNaming")] - private static event EventHandler _FarmerChanged; -#endif - - /********* ** Events *********/ -#if SMAPI_1_x - /// Raised after the player loads a saved game. - [Obsolete("Use " + nameof(SaveEvents) + "." + nameof(SaveEvents.AfterLoad) + " instead")] - public static event EventHandler LoadedGame - { - add - { - PlayerEvents.DeprecationManager.Warn($"{nameof(PlayerEvents)}.{nameof(PlayerEvents.LoadedGame)}", "1.6", DeprecationLevel.PendingRemoval); - PlayerEvents._LoadedGame += value; - } - remove => PlayerEvents._LoadedGame -= value; - } - - /// Raised after the game assigns a new player character. This happens just before ; it's unclear how this would happen any other time. - [Obsolete("should no longer be used")] - public static event EventHandler FarmerChanged - { - add - { - PlayerEvents.DeprecationManager.Warn($"{nameof(PlayerEvents)}.{nameof(PlayerEvents.FarmerChanged)}", "1.6", DeprecationLevel.PendingRemoval); - PlayerEvents._FarmerChanged += value; - } - remove => PlayerEvents._FarmerChanged -= value; - } -#endif - /// Raised after the player's inventory changes in any way (added or removed item, sorted, etc). public static event EventHandler InventoryChanged; @@ -68,32 +22,6 @@ namespace StardewModdingAPI.Events /********* ** Internal methods *********/ -#if SMAPI_1_x - /// Injects types required for backwards compatibility. - /// Manages deprecation warnings. - internal static void Shim(DeprecationManager deprecationManager) - { - PlayerEvents.DeprecationManager = deprecationManager; - } - - /// Raise a event. - /// Encapsulates monitoring and logging. - /// Whether the save has been loaded. This is always true. - internal static void InvokeLoadedGame(IMonitor monitor, EventArgsLoadedGameChanged loaded) - { - monitor.SafelyRaiseGenericEvent($"{nameof(PlayerEvents)}.{nameof(PlayerEvents.LoadedGame)}", PlayerEvents._LoadedGame?.GetInvocationList(), null, loaded); - } - - /// Raise a event. - /// Encapsulates monitoring and logging. - /// The previous player character. - /// The new player character. - internal static void InvokeFarmerChanged(IMonitor monitor, SFarmer priorFarmer, SFarmer newFarmer) - { - monitor.SafelyRaiseGenericEvent($"{nameof(PlayerEvents)}.{nameof(PlayerEvents.FarmerChanged)}", PlayerEvents._FarmerChanged?.GetInvocationList(), null, new EventArgsFarmerChanged(priorFarmer, newFarmer)); - } -#endif - /// Raise an event. /// Encapsulates monitoring and logging. /// The player's inventory. diff --git a/src/StardewModdingAPI/Events/TimeEvents.cs b/src/StardewModdingAPI/Events/TimeEvents.cs index d5ab9fb7..9aea5e04 100644 --- a/src/StardewModdingAPI/Events/TimeEvents.cs +++ b/src/StardewModdingAPI/Events/TimeEvents.cs @@ -1,38 +1,11 @@ -using System; -using System.Diagnostics.CodeAnalysis; +using System; using StardewModdingAPI.Framework; -#pragma warning disable 618 // Suppress obsolete-symbol errors in this file. Since several events are marked obsolete, this produces unnecessary warnings. namespace StardewModdingAPI.Events { /// Events raised when the in-game date or time changes. public static class TimeEvents { - /********* - ** Properties - *********/ -#if SMAPI_1_x - /// Manages deprecation warnings. - private static DeprecationManager DeprecationManager; - - /// The backing field for . - [SuppressMessage("ReSharper", "InconsistentNaming")] - private static event EventHandler _OnNewDay; - - /// The backing field for . - [SuppressMessage("ReSharper", "InconsistentNaming")] - private static event EventHandler _DayOfMonthChanged; - - /// The backing field for . - [SuppressMessage("ReSharper", "InconsistentNaming")] - private static event EventHandler _SeasonOfYearChanged; - - /// The backing field for . - [SuppressMessage("ReSharper", "InconsistentNaming")] - private static event EventHandler _YearOfGameChanged; -#endif - - /********* ** Events *********/ @@ -42,69 +15,9 @@ namespace StardewModdingAPI.Events /// Raised after the in-game clock changes. public static event EventHandler TimeOfDayChanged; -#if SMAPI_1_x - /// Raised after the day-of-month value changes, including when loading a save. This may happen before save; in most cases you should use instead. - [Obsolete("Use " + nameof(TimeEvents) + "." + nameof(TimeEvents.AfterDayStarted) + " or " + nameof(SaveEvents) + " instead")] - public static event EventHandler DayOfMonthChanged - { - add - { - TimeEvents.DeprecationManager.Warn($"{nameof(TimeEvents)}.{nameof(TimeEvents.DayOfMonthChanged)}", "1.14", DeprecationLevel.PendingRemoval); - TimeEvents._DayOfMonthChanged += value; - } - remove => TimeEvents._DayOfMonthChanged -= value; - } - - /// Raised after the year value changes. - [Obsolete("Use " + nameof(TimeEvents) + "." + nameof(TimeEvents.AfterDayStarted) + " or " + nameof(SaveEvents) + " instead")] - public static event EventHandler YearOfGameChanged - { - add - { - TimeEvents.DeprecationManager.Warn($"{nameof(TimeEvents)}.{nameof(TimeEvents.YearOfGameChanged)}", "1.14", DeprecationLevel.PendingRemoval); - TimeEvents._YearOfGameChanged += value; - } - remove => TimeEvents._YearOfGameChanged -= value; - } - - /// Raised after the season value changes. - [Obsolete("Use " + nameof(TimeEvents) + "." + nameof(TimeEvents.AfterDayStarted) + " or " + nameof(SaveEvents) + " instead")] - public static event EventHandler SeasonOfYearChanged - { - add - { - TimeEvents.DeprecationManager.Warn($"{nameof(TimeEvents)}.{nameof(TimeEvents.SeasonOfYearChanged)}", "1.14", DeprecationLevel.PendingRemoval); - TimeEvents._SeasonOfYearChanged += value; - } - remove => TimeEvents._SeasonOfYearChanged -= value; - } - - /// Raised when the player is transitioning to a new day and the game is performing its day update logic. This event is triggered twice: once after the game starts transitioning, and again after it finishes. - [Obsolete("Use " + nameof(TimeEvents) + "." + nameof(TimeEvents.AfterDayStarted) + " or " + nameof(SaveEvents) + " instead")] - public static event EventHandler OnNewDay - { - add - { - TimeEvents.DeprecationManager.Warn($"{nameof(TimeEvents)}.{nameof(TimeEvents.OnNewDay)}", "1.6", DeprecationLevel.PendingRemoval); - TimeEvents._OnNewDay += value; - } - remove => TimeEvents._OnNewDay -= value; - } -#endif - - /********* ** Internal methods *********/ -#if SMAPI_1_x - /// Injects types required for backwards compatibility. - /// Manages deprecation warnings. - internal static void Shim(DeprecationManager deprecationManager) - { - TimeEvents.DeprecationManager = deprecationManager; - } -#endif - /// Raise an event. /// Encapsulates monitoring and logging. internal static void InvokeAfterDayStarted(IMonitor monitor) @@ -120,44 +33,5 @@ namespace StardewModdingAPI.Events { monitor.SafelyRaiseGenericEvent($"{nameof(TimeEvents)}.{nameof(TimeEvents.TimeOfDayChanged)}", TimeEvents.TimeOfDayChanged?.GetInvocationList(), null, new EventArgsIntChanged(priorTime, newTime)); } - -#if SMAPI_1_x - /// Raise a event. - /// Encapsulates monitoring and logging. - /// The previous day value. - /// The current day value. - internal static void InvokeDayOfMonthChanged(IMonitor monitor, int priorDay, int newDay) - { - monitor.SafelyRaiseGenericEvent($"{nameof(TimeEvents)}.{nameof(TimeEvents.DayOfMonthChanged)}", TimeEvents._DayOfMonthChanged?.GetInvocationList(), null, new EventArgsIntChanged(priorDay, newDay)); - } - - /// Raise a event. - /// Encapsulates monitoring and logging. - /// The previous year value. - /// The current year value. - internal static void InvokeYearOfGameChanged(IMonitor monitor, int priorYear, int newYear) - { - monitor.SafelyRaiseGenericEvent($"{nameof(TimeEvents)}.{nameof(TimeEvents.YearOfGameChanged)}", TimeEvents._YearOfGameChanged?.GetInvocationList(), null, new EventArgsIntChanged(priorYear, newYear)); - } - - /// Raise a event. - /// Encapsulates monitoring and logging. - /// The previous season name. - /// The current season name. - internal static void InvokeSeasonOfYearChanged(IMonitor monitor, string priorSeason, string newSeason) - { - monitor.SafelyRaiseGenericEvent($"{nameof(TimeEvents)}.{nameof(TimeEvents.SeasonOfYearChanged)}", TimeEvents._SeasonOfYearChanged?.GetInvocationList(), null, new EventArgsStringChanged(priorSeason, newSeason)); - } - - /// Raise a event. - /// Encapsulates monitoring and logging. - /// The previous day value. - /// The current day value. - /// Whether the game just started the transition (true) or finished it (false). - internal static void InvokeOnNewDay(IMonitor monitor, int priorDay, int newDay, bool isTransitioning) - { - monitor.SafelyRaiseGenericEvent($"{nameof(TimeEvents)}.{nameof(TimeEvents.OnNewDay)}", TimeEvents._OnNewDay?.GetInvocationList(), null, new EventArgsNewDay(priorDay, newDay, isTransitioning)); - } -#endif } } diff --git a/src/StardewModdingAPI/Framework/CursorPosition.cs b/src/StardewModdingAPI/Framework/CursorPosition.cs index 0fb2309b..db02b3d1 100644 --- a/src/StardewModdingAPI/Framework/CursorPosition.cs +++ b/src/StardewModdingAPI/Framework/CursorPosition.cs @@ -1,4 +1,3 @@ -#if !SMAPI_1_x using Microsoft.Xna.Framework; namespace StardewModdingAPI.Framework @@ -34,4 +33,3 @@ namespace StardewModdingAPI.Framework } } } -#endif diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ReflectionHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/ReflectionHelper.cs index 14a339da..8d435416 100644 --- a/src/StardewModdingAPI/Framework/ModHelpers/ReflectionHelper.cs +++ b/src/StardewModdingAPI/Framework/ModHelpers/ReflectionHelper.cs @@ -1,4 +1,4 @@ -using System; +using System; using StardewModdingAPI.Framework.Reflection; namespace StardewModdingAPI.Framework.ModHelpers @@ -180,7 +180,6 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The type being accessed. private void AssertAccessAllowed(Type type) { -#if !SMAPI_1_x // validate type namespace if (type.Namespace != null) { @@ -188,7 +187,6 @@ namespace StardewModdingAPI.Framework.ModHelpers if (type.Namespace == rootSmapiNamespace || type.Namespace.StartsWith(rootSmapiNamespace + ".")) throw new InvalidOperationException($"SMAPI blocked access by {this.ModName} to its internals through the reflection API. Accessing the SMAPI internals is strongly discouraged since they're subject to change, which means the mod can break without warning."); } -#endif } /// Assert that mods can use the reflection helper to access the given type. diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs index 6b19db5c..87b6a99c 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -109,17 +109,6 @@ namespace StardewModdingAPI.Framework.ModLoading ModCompatibility compatibility = mod.Compatibility; if (compatibility?.Compatibility == ModCompatibilityType.AssumeBroken) { -#if SMAPI_1_x - bool hasOfficialUrl = mod.Compatibility.UpdateUrls.Length > 0; - bool hasUnofficialUrl = mod.Compatibility.UpdateUrls.Length > 1; - - string reasonPhrase = compatibility.ReasonPhrase ?? "it's not compatible with the latest version of the game or SMAPI"; - string error = $"{reasonPhrase}. Please check for a version newer than {compatibility.UpperVersionLabel ?? compatibility.UpperVersion.ToString()} here:"; - if (hasOfficialUrl) - error += !hasUnofficialUrl ? $" {compatibility.UpdateUrls[0]}" : $"{Environment.NewLine}- official mod: {compatibility.UpdateUrls[0]}"; - if (hasUnofficialUrl) - error += $"{Environment.NewLine}- unofficial update: {compatibility.UpdateUrls[1]}"; -#else string reasonPhrase = compatibility.ReasonPhrase ?? "it's no longer compatible"; string error = $"{reasonPhrase}. Please check for a "; if (mod.Manifest.Version.Equals(compatibility.UpperVersion) && compatibility.UpperVersionLabel == null) @@ -127,7 +116,6 @@ namespace StardewModdingAPI.Framework.ModLoading else error += $"version newer than {compatibility.UpperVersionLabel ?? compatibility.UpperVersion.ToString()}"; error += " at " + string.Join(" or ", compatibility.UpdateUrls); -#endif mod.SetStatus(ModMetadataStatus.Failed, error); continue; @@ -150,7 +138,6 @@ namespace StardewModdingAPI.Framework.ModLoading } // validate required fields -#if !SMAPI_1_x { List missingFields = new List(3); @@ -164,11 +151,9 @@ namespace StardewModdingAPI.Framework.ModLoading if (missingFields.Any()) mod.SetStatus(ModMetadataStatus.Failed, $"its manifest is missing required fields ({string.Join(", ", missingFields)})."); } -#endif } // validate IDs are unique -#if !SMAPI_1_x { var duplicatesByID = mods .GroupBy(mod => mod.Manifest?.UniqueID?.Trim(), mod => mod, StringComparer.InvariantCultureIgnoreCase) @@ -183,7 +168,6 @@ namespace StardewModdingAPI.Framework.ModLoading } } } -#endif } /// Sort the given mods by the order they should be loaded. @@ -264,12 +248,7 @@ namespace StardewModdingAPI.Framework.ModLoading ID = entry.UniqueID, MinVersion = entry.MinimumVersion, Mod = dependencyMod, - IsRequired = -#if SMAPI_1_x - true -#else - entry.IsRequired -#endif + IsRequired = entry.IsRequired } ) .ToArray(); diff --git a/src/StardewModdingAPI/Framework/ModRegistry.cs b/src/StardewModdingAPI/Framework/ModRegistry.cs index 8f30d813..9dde7a20 100644 --- a/src/StardewModdingAPI/Framework/ModRegistry.cs +++ b/src/StardewModdingAPI/Framework/ModRegistry.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -42,11 +42,7 @@ namespace StardewModdingAPI.Framework uniqueID = uniqueID.Trim(); // find match - return this.GetAll().FirstOrDefault(p => -#if SMAPI_1_x - p.UniqueID != null && -#endif - p.UniqueID.Trim().Equals(uniqueID, StringComparison.InvariantCultureIgnoreCase)); + return this.GetAll().FirstOrDefault(p => p.UniqueID.Trim().Equals(uniqueID, StringComparison.InvariantCultureIgnoreCase)); } /// Get whether a mod has been loaded. diff --git a/src/StardewModdingAPI/Framework/Models/Manifest.cs b/src/StardewModdingAPI/Framework/Models/Manifest.cs index f97cb8ff..2e9566bf 100644 --- a/src/StardewModdingAPI/Framework/Models/Manifest.cs +++ b/src/StardewModdingAPI/Framework/Models/Manifest.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using Newtonsoft.Json; using StardewModdingAPI.Framework.Serialisation; @@ -35,23 +34,15 @@ namespace StardewModdingAPI.Framework.Models [JsonConverter(typeof(SFieldConverter))] public IManifestDependency[] Dependencies { get; set; } -#if !SMAPI_1_x /// The mod's unique ID in Nexus Mods (if any), used for update checks. public string NexusID { get; set; } /// The mod's organisation and project name on GitHub (if any), used for update checks. public string GitHubProject { get; set; } -#endif /// The unique mod ID. public string UniqueID { get; set; } -#if SMAPI_1_x - /// Whether the mod uses per-save config files. - [Obsolete("Use " + nameof(Mod) + "." + nameof(Mod.Helper) + "." + nameof(IModHelper.ReadConfig) + " instead")] - public bool PerSaveConfigs { get; set; } -#endif - /// Any manifest fields which didn't match a valid field. [JsonExtensionData] public IDictionary ExtraFields { get; set; } diff --git a/src/StardewModdingAPI/Framework/Models/ManifestDependency.cs b/src/StardewModdingAPI/Framework/Models/ManifestDependency.cs index 67f906e3..5646b335 100644 --- a/src/StardewModdingAPI/Framework/Models/ManifestDependency.cs +++ b/src/StardewModdingAPI/Framework/Models/ManifestDependency.cs @@ -1,4 +1,4 @@ -namespace StardewModdingAPI.Framework.Models +namespace StardewModdingAPI.Framework.Models { /// A mod dependency listed in a mod manifest. internal class ManifestDependency : IManifestDependency @@ -12,10 +12,8 @@ /// The minimum required version (if any). public ISemanticVersion MinimumVersion { get; set; } -#if !SMAPI_1_x /// Whether the dependency must be installed to use the mod. public bool IsRequired { get; set; } -#endif /********* ** Public methods @@ -24,19 +22,13 @@ /// The unique mod ID to require. /// The minimum required version (if any). /// Whether the dependency must be installed to use the mod. - public ManifestDependency(string uniqueID, string minimumVersion -#if !SMAPI_1_x - , bool required = true -#endif - ) + public ManifestDependency(string uniqueID, string minimumVersion, bool required = true) { this.UniqueID = uniqueID; this.MinimumVersion = !string.IsNullOrWhiteSpace(minimumVersion) ? new SemanticVersion(minimumVersion) : null; -#if !SMAPI_1_x this.IsRequired = required; -#endif } } } diff --git a/src/StardewModdingAPI/Framework/Monitor.cs b/src/StardewModdingAPI/Framework/Monitor.cs index c2c3a689..bf338386 100644 --- a/src/StardewModdingAPI/Framework/Monitor.cs +++ b/src/StardewModdingAPI/Framework/Monitor.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -37,10 +37,8 @@ namespace StardewModdingAPI.Framework /// Whether SMAPI is aborting. Mods don't need to worry about this unless they have background tasks. public bool IsExiting => this.ExitTokenSource.IsCancellationRequested; -#if !SMAPI_1_x /// Whether to show the full log stamps (with time/level/logger) in the console. If false, shows a simplified stamp with only the logger. internal bool ShowFullStampInConsole { get; set; } -#endif /// Whether to show trace messages in the console. internal bool ShowTraceInConsole { get; set; } @@ -89,7 +87,6 @@ namespace StardewModdingAPI.Framework this.ExitTokenSource.Cancel(); } -#if !SMAPI_1_x /// Write a newline to the console and log file. internal void Newline() { @@ -98,20 +95,6 @@ namespace StardewModdingAPI.Framework if (this.WriteToFile) this.LogFile.WriteLine(""); } -#endif - -#if SMAPI_1_x - /// Log a message for the player or developer, using the specified console color. - /// The name of the mod logging the message. - /// The message to log. - /// The console color. - /// The log level. - [Obsolete("This method is provided for backwards compatibility and otherwise should not be used. Use " + nameof(Monitor) + "." + nameof(Monitor.Log) + " instead.")] - internal void LegacyLog(string source, string message, ConsoleColor color, LogLevel level = LogLevel.Debug) - { - this.LogImpl(source, message, level, color); - } -#endif /********* @@ -136,11 +119,7 @@ namespace StardewModdingAPI.Framework string levelStr = level.ToString().ToUpper().PadRight(Monitor.MaxLevelLength); string fullMessage = $"[{DateTime.Now:HH:mm:ss} {levelStr} {source}] {message}"; -#if !SMAPI_1_x string consoleMessage = this.ShowFullStampInConsole ? fullMessage : $"[{source}] {message}"; -#else - string consoleMessage = fullMessage; -#endif // write to console if (this.WriteToConsole && (this.ShowTraceInConsole || level != LogLevel.Trace)) @@ -168,11 +147,9 @@ namespace StardewModdingAPI.Framework /// Get the color scheme to use for the current console. private static IDictionary GetConsoleColorScheme() { -#if !SMAPI_1_x // scheme for dark console background if (Monitor.IsDark(Console.BackgroundColor)) { -#endif return new Dictionary { [LogLevel.Trace] = ConsoleColor.DarkGray, @@ -182,7 +159,6 @@ namespace StardewModdingAPI.Framework [LogLevel.Error] = ConsoleColor.Red, [LogLevel.Alert] = ConsoleColor.Magenta }; -#if !SMAPI_1_x } // scheme for light console background @@ -195,7 +171,6 @@ namespace StardewModdingAPI.Framework [LogLevel.Error] = ConsoleColor.Red, [LogLevel.Alert] = ConsoleColor.DarkMagenta }; -#endif } /// Get whether a console color should be considered dark, which is subjectively defined as 'white looks better than black on this text'. diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index 387aeacc..7287cab7 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -20,9 +20,6 @@ using StardewValley.Menus; using StardewValley.Tools; using xTile.Dimensions; using xTile.Layers; -#if SMAPI_1_x -using SFarmer = StardewValley.Farmer; -#endif namespace StardewModdingAPI.Framework { @@ -117,23 +114,6 @@ namespace StardewModdingAPI.Framework /// The time of day (in 24-hour military format) at last check. private int PreviousTime; -#if SMAPI_1_x - /// The day of month (1–28) at last check. - private int PreviousDay; - - /// The season name (winter, spring, summer, or fall) at last check. - private string PreviousSeason; - - /// The year number at last check. - private int PreviousYear; - - /// Whether the game was transitioning to a new day at last check. - private bool PreviousIsNewDay; - - /// The player character at last check. - private SFarmer PreviousFarmer; -#endif - /// The previous content locale. private LocalizedContentManager.LanguageCode? PreviousLocale; @@ -164,10 +144,10 @@ namespace StardewModdingAPI.Framework private Color bgColor => SGame.Reflection.GetPrivateField(this, nameof(bgColor)).GetValue(); public RenderTarget2D screenWrapper => SGame.Reflection.GetPrivateProperty(this, "screen").GetValue(); // deliberately renamed to avoid an infinite loop public BlendState lightingBlend => SGame.Reflection.GetPrivateField(this, nameof(lightingBlend)).GetValue(); - private readonly Action drawFarmBuildings = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(drawFarmBuildings)).Invoke(new object[0]); - private readonly Action drawHUD = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(drawHUD)).Invoke(new object[0]); - private readonly Action drawDialogueBox = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(drawDialogueBox)).Invoke(new object[0]); - private readonly Action renderScreenBuffer = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(renderScreenBuffer)).Invoke(new object[0]); + private readonly Action drawFarmBuildings = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(drawFarmBuildings)).Invoke(); + private readonly Action drawHUD = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(drawHUD)).Invoke(); + private readonly Action drawDialogueBox = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(drawDialogueBox)).Invoke(); + private readonly Action renderScreenBuffer = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(renderScreenBuffer)).Invoke(); // ReSharper restore ArrangeStaticMemberQualifier, ArrangeThisQualifier, InconsistentNaming @@ -295,10 +275,6 @@ namespace StardewModdingAPI.Framework if (this.FirstUpdate) { GameEvents.InvokeInitialize(this.Monitor); -#if SMAPI_1_x - GameEvents.InvokeLoadContent(this.Monitor); - GameEvents.InvokeGameLoaded(this.Monitor); -#endif } /********* @@ -321,10 +297,8 @@ namespace StardewModdingAPI.Framework *********/ if (Context.IsSaveLoaded && !SaveGame.IsProcessing /*still loading save*/ && this.AfterLoadTimer >= 0) { -#if !SMAPI_1_x if (Game1.dayOfMonth != 0) // wait until new-game intro finishes (world not fully initialised yet) -#endif - this.AfterLoadTimer--; + this.AfterLoadTimer--; if (this.AfterLoadTimer == 0) { @@ -332,9 +306,6 @@ namespace StardewModdingAPI.Framework Context.IsWorldReady = true; SaveEvents.InvokeAfterLoad(this.Monitor); -#if SMAPI_1_x - PlayerEvents.InvokeLoadedGame(this.Monitor, new EventArgsLoadedGameChanged(Game1.hasLoadedGame)); -#endif TimeEvents.InvokeAfterDayStarted(this.Monitor); } } @@ -403,7 +374,6 @@ namespace StardewModdingAPI.Framework bool isClick = framePressedKeys.Contains(SButton.MouseLeft) || (framePressedKeys.Contains(SButton.ControllerA) && !currentlyPressedKeys.Contains(SButton.ControllerX)); // get cursor position -#if !SMAPI_1_x ICursorPosition cursor; { // cursor position @@ -414,14 +384,11 @@ namespace StardewModdingAPI.Framework : Game1.player.GetGrabTile(); cursor = new CursorPosition(screenPixels, tile, grabTile); } -#endif // raise button pressed foreach (SButton button in framePressedKeys) { -#if !SMAPI_1_x InputEvents.InvokeButtonPressed(this.Monitor, button, cursor, isClick); -#endif // legacy events if (button.TryGetKeyboard(out Keys key)) @@ -441,12 +408,10 @@ namespace StardewModdingAPI.Framework // raise button released foreach (SButton button in frameReleasedKeys) { -#if !SMAPI_1_x bool wasClick = (button == SButton.MouseLeft && previousPressedKeys.Contains(SButton.MouseLeft)) // released left click || (button == SButton.ControllerA && previousPressedKeys.Contains(SButton.ControllerA) && !previousPressedKeys.Contains(SButton.ControllerX)); InputEvents.InvokeButtonReleased(this.Monitor, button, cursor, wasClick); -#endif // legacy events if (button.TryGetKeyboard(out Keys key)) @@ -524,12 +489,6 @@ namespace StardewModdingAPI.Framework if (this.GetHash(Game1.locations) != this.PreviousGameLocations) LocationEvents.InvokeLocationsChanged(this.Monitor, Game1.locations); -#if SMAPI_1_x - // raise player changed - if (Game1.player != this.PreviousFarmer) - PlayerEvents.InvokeFarmerChanged(this.Monitor, this.PreviousFarmer, Game1.player); -#endif - // raise events that shouldn't be triggered on initial load if (Game1.uniqueIDForThisGame == this.PreviousSaveID) { @@ -559,14 +518,6 @@ namespace StardewModdingAPI.Framework // raise time changed if (Game1.timeOfDay != this.PreviousTime) TimeEvents.InvokeTimeOfDayChanged(this.Monitor, this.PreviousTime, Game1.timeOfDay); -#if SMAPI_1_x - if (Game1.dayOfMonth != this.PreviousDay) - TimeEvents.InvokeDayOfMonthChanged(this.Monitor, this.PreviousDay, Game1.dayOfMonth); - if (Game1.currentSeason != this.PreviousSeason) - TimeEvents.InvokeSeasonOfYearChanged(this.Monitor, this.PreviousSeason, Game1.currentSeason); - if (Game1.year != this.PreviousYear) - TimeEvents.InvokeYearOfGameChanged(this.Monitor, this.PreviousYear, Game1.year); -#endif // raise mine level changed if (Game1.mine != null && Game1.mine.mineLevel != this.PreviousMineLevel) @@ -587,25 +538,8 @@ namespace StardewModdingAPI.Framework this.PreviousTime = Game1.timeOfDay; this.PreviousMineLevel = Game1.mine?.mineLevel ?? 0; this.PreviousSaveID = Game1.uniqueIDForThisGame; -#if SMAPI_1_x - this.PreviousFarmer = Game1.player; - this.PreviousDay = Game1.dayOfMonth; - this.PreviousSeason = Game1.currentSeason; - this.PreviousYear = Game1.year; -#endif } - /********* - ** Game day transition event (obsolete) - *********/ -#if SMAPI_1_x - if (Game1.newDay != this.PreviousIsNewDay) - { - TimeEvents.InvokeOnNewDay(this.Monitor, this.PreviousDay, Game1.dayOfMonth, Game1.newDay); - this.PreviousIsNewDay = Game1.newDay; - } -#endif - /********* ** Game update *********/ @@ -623,12 +557,7 @@ namespace StardewModdingAPI.Framework *********/ GameEvents.InvokeUpdateTick(this.Monitor); if (this.FirstUpdate) - { -#if SMAPI_1_x - GameEvents.InvokeFirstUpdateTick(this.Monitor); -#endif this.FirstUpdate = false; - } if (this.CurrentUpdateTick % 2 == 0) GameEvents.InvokeSecondUpdateTick(this.Monitor); if (this.CurrentUpdateTick % 4 == 0) diff --git a/src/StardewModdingAPI/Framework/Serialisation/SFieldConverter.cs b/src/StardewModdingAPI/Framework/Serialisation/SFieldConverter.cs index 11ffdccb..5419896f 100644 --- a/src/StardewModdingAPI/Framework/Serialisation/SFieldConverter.cs +++ b/src/StardewModdingAPI/Framework/Serialisation/SFieldConverter.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -76,12 +76,8 @@ namespace StardewModdingAPI.Framework.Serialisation { string uniqueID = obj.Value(nameof(IManifestDependency.UniqueID)); string minVersion = obj.Value(nameof(IManifestDependency.MinimumVersion)); -#if SMAPI_1_x - result.Add(new ManifestDependency(uniqueID, minVersion)); -#else bool required = obj.Value(nameof(IManifestDependency.IsRequired)) ?? true; result.Add(new ManifestDependency(uniqueID, minVersion, required)); -#endif } return result.ToArray(); } diff --git a/src/StardewModdingAPI/Framework/Utilities/ContextHash.cs b/src/StardewModdingAPI/Framework/Utilities/ContextHash.cs index 0d8487bb..6c0fdc90 100644 --- a/src/StardewModdingAPI/Framework/Utilities/ContextHash.cs +++ b/src/StardewModdingAPI/Framework/Utilities/ContextHash.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Generic; namespace StardewModdingAPI.Framework.Utilities @@ -25,7 +24,7 @@ namespace StardewModdingAPI.Framework.Utilities /// The specified key is already added. public void Track(T key, Action action) { - if(this.Contains(key)) + if (this.Contains(key)) throw new InvalidOperationException($"Can't track context for key {key} because it's already added."); this.Add(key); diff --git a/src/StardewModdingAPI/IContentHelper.cs b/src/StardewModdingAPI/IContentHelper.cs index b4557134..b78b165b 100644 --- a/src/StardewModdingAPI/IContentHelper.cs +++ b/src/StardewModdingAPI/IContentHelper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; @@ -12,13 +12,11 @@ namespace StardewModdingAPI /********* ** Accessors *********/ -#if !SMAPI_1_x /// Interceptors which provide the initial versions of matching content assets. IList AssetLoaders { get; } /// Interceptors which edit matching content assets after they're loaded. IList AssetEditors { get; } -#endif /// The game's current locale code (like pt-BR). string CurrentLocale { get; } @@ -44,7 +42,6 @@ namespace StardewModdingAPI /// The is empty or contains invalid characters. string GetActualAssetKey(string key, ContentSource source = ContentSource.ModFolder); -#if !SMAPI_1_x /// Remove an asset from the content cache so it's reloaded on the next request. This will reload core game assets if needed, but references to the former asset will still show the previous content. /// The asset key to invalidate in the content folder. /// The is empty or contains invalid characters. @@ -55,6 +52,5 @@ namespace StardewModdingAPI /// The asset type to remove from the cache. /// Returns whether any assets were invalidated. bool InvalidateCache(); -#endif } } diff --git a/src/StardewModdingAPI/ICursorPosition.cs b/src/StardewModdingAPI/ICursorPosition.cs index 8fbc115f..ddb8eb49 100644 --- a/src/StardewModdingAPI/ICursorPosition.cs +++ b/src/StardewModdingAPI/ICursorPosition.cs @@ -1,4 +1,3 @@ -#if !SMAPI_1_x using Microsoft.Xna.Framework; namespace StardewModdingAPI @@ -16,4 +15,3 @@ namespace StardewModdingAPI Vector2 GrabTile { get; } } } -#endif diff --git a/src/StardewModdingAPI/IManifestDependency.cs b/src/StardewModdingAPI/IManifestDependency.cs index 1fa6c812..e86cd1f4 100644 --- a/src/StardewModdingAPI/IManifestDependency.cs +++ b/src/StardewModdingAPI/IManifestDependency.cs @@ -1,4 +1,4 @@ -namespace StardewModdingAPI +namespace StardewModdingAPI { /// A mod dependency listed in a mod manifest. public interface IManifestDependency @@ -12,9 +12,7 @@ /// The minimum required version (if any). ISemanticVersion MinimumVersion { get; } -#if !SMAPI_1_x /// Whether the dependency must be installed to use the mod. bool IsRequired { get; } -#endif } } diff --git a/src/StardewModdingAPI/ISemanticVersion.cs b/src/StardewModdingAPI/ISemanticVersion.cs index c1a4ca3a..0483c97b 100644 --- a/src/StardewModdingAPI/ISemanticVersion.cs +++ b/src/StardewModdingAPI/ISemanticVersion.cs @@ -1,12 +1,9 @@ -using System; +using System; namespace StardewModdingAPI { /// A semantic version with an optional release tag. - public interface ISemanticVersion : IComparable -#if !SMAPI_1_x - , IEquatable -#endif + public interface ISemanticVersion : IComparable, IEquatable { /********* ** Accessors @@ -59,4 +56,4 @@ namespace StardewModdingAPI /// Get a string representation of the version. string ToString(); } -} \ No newline at end of file +} diff --git a/src/StardewModdingAPI/Log.cs b/src/StardewModdingAPI/Log.cs deleted file mode 100644 index 60220ad8..00000000 --- a/src/StardewModdingAPI/Log.cs +++ /dev/null @@ -1,320 +0,0 @@ -#if SMAPI_1_x -using System; -using System.Threading; -using StardewModdingAPI.Framework; -using Monitor = StardewModdingAPI.Framework.Monitor; - -namespace StardewModdingAPI -{ - /// A singleton which logs messages to the SMAPI console and log file. - [Obsolete("Use " + nameof(Mod) + "." + nameof(Mod.Monitor))] - public static class Log - { - /********* - ** Properties - *********/ - /// Manages deprecation warnings. - private static DeprecationManager DeprecationManager; - - /// The underlying logger. - private static Monitor Monitor; - - /// Tracks the installed mods. - private static ModRegistry ModRegistry; - - - /********* - ** Public methods - *********/ - /// Injects types required for backwards compatibility. - /// Manages deprecation warnings. - /// The underlying logger. - /// Tracks the installed mods. - internal static void Shim(DeprecationManager deprecationManager, Monitor monitor, ModRegistry modRegistry) - { - Log.DeprecationManager = deprecationManager; - Log.Monitor = monitor; - Log.ModRegistry = modRegistry; - } - - /**** - ** Exceptions - ****/ - /// Log an exception event. - /// The event sender. - /// The event arguments. - public static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) - { - Log.WarnDeprecated(); - Log.Monitor.Log($"Critical app domain exception: {e.ExceptionObject}", LogLevel.Error); - } - - /// Log a thread exception event. - /// The event sender. - /// The event arguments. - public static void Application_ThreadException(object sender, ThreadExceptionEventArgs e) - { - Log.WarnDeprecated(); - Log.Monitor.Log($"Critical thread exception: {e.Exception}", LogLevel.Error); - } - - /**** - ** Synchronous logging - ****/ - /// Synchronously log a message to the console. NOTE: synchronous logging is discouraged; use asynchronous methods instead. - /// The message to log. - /// The message color. - public static void SyncColour(object message, ConsoleColor color) - { - Log.WarnDeprecated(); - Log.Monitor.LegacyLog(Log.GetModName(), message?.ToString(), color); - } - - /**** - ** Asynchronous logging - ****/ - /// Asynchronously log a message to the console with the specified color. - /// The message to log. - /// The message color. - public static void AsyncColour(object message, ConsoleColor color) - { - Log.WarnDeprecated(); - Log.Monitor.LegacyLog(Log.GetModName(), message?.ToString(), color); - } - - /// Asynchronously log a message to the console. - /// The message to log. - public static void Async(object message) - { - Log.WarnDeprecated(); - Log.Monitor.LegacyLog(Log.GetModName(), message?.ToString(), ConsoleColor.Gray); - } - - /// Asynchronously log a red message to the console. - /// The message to log. - public static void AsyncR(object message) - { - Log.WarnDeprecated(); - Log.Monitor.LegacyLog(Log.GetModName(), message?.ToString(), ConsoleColor.Red); - } - - /// Asynchronously log an orange message to the console. - /// The message to log. - public static void AsyncO(object message) - { - Log.WarnDeprecated(); - Log.Monitor.LegacyLog(Log.GetModName(), message?.ToString(), ConsoleColor.DarkYellow); - } - - /// Asynchronously log a yellow message to the console. - /// The message to log. - public static void AsyncY(object message) - { - Log.WarnDeprecated(); - Log.Monitor.LegacyLog(Log.GetModName(), message?.ToString(), ConsoleColor.Yellow); - } - - /// Asynchronously log a green message to the console. - /// The message to log. - public static void AsyncG(object message) - { - Log.WarnDeprecated(); - Log.Monitor.LegacyLog(Log.GetModName(), message?.ToString(), ConsoleColor.Green); - } - - /// Asynchronously log a cyan message to the console. - /// The message to log. - public static void AsyncC(object message) - { - Log.WarnDeprecated(); - Log.Monitor.LegacyLog(Log.GetModName(), message?.ToString(), ConsoleColor.Cyan); - } - - /// Asynchronously log a magenta message to the console. - /// The message to log. - public static void AsyncM(object message) - { - Log.WarnDeprecated(); - Log.Monitor.LegacyLog(Log.GetModName(), message?.ToString(), ConsoleColor.Magenta); - } - - /// Asynchronously log a warning to the console. - /// The message to log. - public static void Warning(object message) - { - Log.WarnDeprecated(); - Log.Monitor.LegacyLog(Log.GetModName(), message?.ToString(), ConsoleColor.Yellow, LogLevel.Warn); - } - - /// Asynchronously log an error to the console. - /// The message to log. - public static void Error(object message) - { - Log.WarnDeprecated(); - Log.Monitor.LegacyLog(Log.GetModName(), message?.ToString(), ConsoleColor.Red, LogLevel.Error); - } - - /// Asynchronously log a success message to the console. - /// The message to log. - public static void Success(object message) - { - Log.WarnDeprecated(); - Log.AsyncG(message); - } - - /// Asynchronously log an info message to the console. - /// The message to log. - public static void Info(object message) - { - Log.WarnDeprecated(); - Log.Monitor.LegacyLog(Log.GetModName(), message?.ToString(), ConsoleColor.White, LogLevel.Info); - } - - /// Asynchronously log an info message to the console. - /// The message to log. - public static void Out(object message) - { - Log.WarnDeprecated(); - Log.Async($"[OUT] {message}"); - } - - /// Asynchronously log a debug message to the console. - /// The message to log. - public static void Debug(object message) - { - Log.WarnDeprecated(); - Log.Monitor.LegacyLog(Log.GetModName(), message?.ToString(), ConsoleColor.DarkGray); - } - - /// Asynchronously log a message to the file that's not shown in the console. - /// The message to log. - internal static void LogToFile(string message) - { - Log.WarnDeprecated(); - Log.Monitor.LegacyLog(Log.GetModName(), message, ConsoleColor.DarkGray, LogLevel.Trace); - } - - /// Obsolete. - public static void LogValueNotSpecified() - { - Log.WarnDeprecated(); - Log.AsyncR(" must be specified"); - } - - /// Obsolete. - public static void LogObjectValueNotSpecified() - { - Log.WarnDeprecated(); - Log.AsyncR(" and must be specified"); - } - - /// Obsolete. - public static void LogValueInvalid() - { - Log.WarnDeprecated(); - Log.AsyncR(" is invalid"); - } - - /// Obsolete. - public static void LogObjectInvalid() - { - Log.WarnDeprecated(); - Log.AsyncR(" is invalid"); - } - - /// Obsolete. - public static void LogValueNotInt32() - { - Log.WarnDeprecated(); - Log.AsyncR(" must be a whole number (Int32)"); - } - - /// Obsolete. - /// The message to log. - /// Obsolete. - /// Obsolete. - [Obsolete("Parameter 'values' is no longer supported. Format before logging.")] - private static void PrintLog(object message, bool disableLogging, params object[] values) - { - Log.WarnDeprecated(); - Log.Monitor.LegacyLog(Log.GetModName(), message?.ToString(), ConsoleColor.Gray); - } - - /// Obsolete. - /// The message to log. - /// Obsolete. - [Obsolete("Parameter 'values' is no longer supported. Format before logging.")] - public static void Success(object message, params object[] values) - { - Log.WarnDeprecated(); - Log.Success(message); - } - - /// Obsolete. - /// The message to log. - /// Obsolete. - [Obsolete("Parameter 'values' is no longer supported. Format before logging.")] - public static void Verbose(object message, params object[] values) - { - Log.WarnDeprecated(); - Log.Out(message); - } - - /// Obsolete. - /// The message to log. - /// Obsolete. - [Obsolete("Parameter 'values' is no longer supported. Format before logging.")] - public static void Comment(object message, params object[] values) - { - Log.WarnDeprecated(); - Log.AsyncC(message); - } - - /// Obsolete. - /// The message to log. - /// Obsolete. - [Obsolete("Parameter 'values' is no longer supported. Format before logging.")] - public static void Info(object message, params object[] values) - { - Log.WarnDeprecated(); - Log.Info(message); - } - - /// Obsolete. - /// The message to log. - /// Obsolete. - [Obsolete("Parameter 'values' is no longer supported. Format before logging.")] - public static void Error(object message, params object[] values) - { - Log.WarnDeprecated(); - Log.Error(message); - } - - /// Obsolete. - /// The message to log. - /// Obsolete. - [Obsolete("Parameter 'values' is no longer supported. Format before logging.")] - public static void Debug(object message, params object[] values) - { - Log.WarnDeprecated(); - Log.Debug(message); - } - - - /********* - ** Private methods - *********/ - /// Raise a deprecation warning. - private static void WarnDeprecated() - { - Log.DeprecationManager.Warn($"the {nameof(Log)} class", "1.1", DeprecationLevel.PendingRemoval); - } - - /// Get the name of the mod logging a message from the stack. - private static string GetModName() - { - return Log.ModRegistry.GetModFromStack() ?? ""; - } - } -} -#endif \ No newline at end of file diff --git a/src/StardewModdingAPI/Metadata/InstructionMetadata.cs b/src/StardewModdingAPI/Metadata/InstructionMetadata.cs index 79fabbd2..35989801 100644 --- a/src/StardewModdingAPI/Metadata/InstructionMetadata.cs +++ b/src/StardewModdingAPI/Metadata/InstructionMetadata.cs @@ -44,7 +44,6 @@ namespace StardewModdingAPI.Metadata new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPreRenderGuiEventNoCheck", InstructionHandleResult.NotCompatible), // APIs removed in SMAPI 2.0 -#if !SMAPI_1_x new TypeFinder("StardewModdingAPI.Command", InstructionHandleResult.NotCompatible), new TypeFinder("StardewModdingAPI.Config", InstructionHandleResult.NotCompatible), new TypeFinder("StardewModdingAPI.Log", InstructionHandleResult.NotCompatible), @@ -67,7 +66,6 @@ namespace StardewModdingAPI.Metadata new PropertyFinder("StardewModdingAPI.Mod", "BaseConfigPath", InstructionHandleResult.NotCompatible), new PropertyFinder("StardewModdingAPI.Mod", "PerSaveConfigFolder", InstructionHandleResult.NotCompatible), new PropertyFinder("StardewModdingAPI.Mod", "PerSaveConfigPath", InstructionHandleResult.NotCompatible), -#endif /**** ** detect code which may impact game stability diff --git a/src/StardewModdingAPI/Mod.cs b/src/StardewModdingAPI/Mod.cs index b5607234..c511ce5a 100644 --- a/src/StardewModdingAPI/Mod.cs +++ b/src/StardewModdingAPI/Mod.cs @@ -1,26 +1,10 @@ -using System; -using System.IO; -using StardewModdingAPI.Framework; -using StardewModdingAPI.Framework.Models; +using System; namespace StardewModdingAPI { /// The base class for a mod. public class Mod : IMod, IDisposable { - /********* - ** Properties - *********/ -#if SMAPI_1_x - /// Manages deprecation warnings. - private static DeprecationManager DeprecationManager; - - - /// The backing field for . - private string _pathOnDisk; -#endif - - /********* ** Accessors *********/ @@ -33,65 +17,10 @@ namespace StardewModdingAPI /// The mod's manifest. public IManifest ModManifest { get; internal set; } -#if SMAPI_1_x - /// The full path to the mod's directory on the disk. - [Obsolete("Use " + nameof(Mod.Helper) + "." + nameof(IModHelper.DirectoryPath) + " instead")] - public string PathOnDisk - { - get - { - Mod.DeprecationManager.Warn($"{nameof(Mod)}.{nameof(Mod.PathOnDisk)}", "1.0", DeprecationLevel.PendingRemoval); - return this._pathOnDisk; - } - internal set { this._pathOnDisk = value; } - } - - /// The full path to the mod's config.json file on the disk. - [Obsolete("Use " + nameof(Mod.Helper) + "." + nameof(IModHelper.ReadConfig) + " instead")] - public string BaseConfigPath - { - get - { - Mod.DeprecationManager.Warn($"{nameof(Mod)}.{nameof(Mod.BaseConfigPath)}", "1.0", DeprecationLevel.PendingRemoval); - Mod.DeprecationManager.MarkWarned($"{nameof(Mod)}.{nameof(Mod.PathOnDisk)}", "1.0"); // avoid redundant warnings - return Path.Combine(this.PathOnDisk, "config.json"); - } - } - - /// The full path to the per-save configs folder (if is true). - [Obsolete("Use " + nameof(Mod.Helper) + "." + nameof(IModHelper.ReadJsonFile) + " instead")] - public string PerSaveConfigFolder => this.GetPerSaveConfigFolder(); - - /// The full path to the per-save configuration file for the current save (if is true). - [Obsolete("Use " + nameof(Mod.Helper) + "." + nameof(IModHelper.ReadJsonFile) + " instead")] - public string PerSaveConfigPath - { - get - { - Mod.DeprecationManager.Warn($"{nameof(Mod)}.{nameof(Mod.PerSaveConfigPath)}", "1.0", DeprecationLevel.PendingRemoval); - Mod.DeprecationManager.MarkWarned($"{nameof(Mod)}.{nameof(Mod.PerSaveConfigFolder)}", "1.0"); // avoid redundant warnings - return Context.IsSaveLoaded ? Path.Combine(this.PerSaveConfigFolder, $"{Constants.SaveFolderName}.json") : ""; - } - } -#endif - /********* ** Public methods *********/ -#if SMAPI_1_x - /// Injects types required for backwards compatibility. - /// Manages deprecation warnings. - internal static void Shim(DeprecationManager deprecationManager) - { - Mod.DeprecationManager = deprecationManager; - } - - /// The mod entry point, called after the mod is first loaded. - [Obsolete("This overload is obsolete since SMAPI 1.0.")] - public virtual void Entry(params object[] objects) { } -#endif - /// The mod entry point, called after the mod is first loaded. /// Provides simplified APIs for writing mods. public virtual void Entry(IModHelper helper) { } @@ -108,23 +37,6 @@ namespace StardewModdingAPI /********* ** Private methods *********/ -#if SMAPI_1_x - /// Get the full path to the per-save configuration file for the current save (if is true). - [Obsolete] - private string GetPerSaveConfigFolder() - { - Mod.DeprecationManager.Warn($"{nameof(Mod)}.{nameof(Mod.PerSaveConfigFolder)}", "1.0", DeprecationLevel.PendingRemoval); - Mod.DeprecationManager.MarkWarned($"{nameof(Mod)}.{nameof(Mod.PathOnDisk)}", "1.0"); // avoid redundant warnings - - if (!((Manifest)this.ModManifest).PerSaveConfigs) - { - this.Monitor.Log("Tried to fetch the per-save config folder, but this mod isn't configured to use per-save config files.", LogLevel.Error); - return ""; - } - return Path.Combine(this.PathOnDisk, "psconfigs"); - } -#endif - /// Release or reset unmanaged resources when the game exits. There's no guarantee this will be called on every exit. /// Whether the instance is being disposed explicitly rather than finalised. If this is false, the instance shouldn't dispose other objects since they may already be finalised. protected virtual void Dispose(bool disposing) { } diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index cee3aefd..8f32b9eb 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -129,9 +129,6 @@ namespace StardewModdingAPI this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GameVersion} on {this.GetFriendlyPlatformName()}", LogLevel.Info); this.Monitor.Log($"Mods go here: {Constants.ModPath}"); this.Monitor.Log($"Log started at {DateTime.UtcNow:s} UTC", LogLevel.Trace); -#if SMAPI_1_x - this.Monitor.Log("Preparing SMAPI..."); -#endif // validate paths this.VerifyPath(Constants.ModPath); @@ -213,11 +210,7 @@ namespace StardewModdingAPI } // start game -#if SMAPI_1_x - this.Monitor.Log("Starting game..."); -#else this.Monitor.Log("Starting game...", LogLevel.Trace); -#endif try { this.IsGameRunning = true; @@ -234,16 +227,6 @@ namespace StardewModdingAPI } } -#if SMAPI_1_x - /// Get a monitor for legacy code which doesn't have one passed in. - [Obsolete("This method should only be used when needed for backwards compatibility.")] - internal IMonitor GetLegacyMonitorForMod() - { - string modName = this.ModRegistry.GetModFromStack() ?? "unknown"; - return this.GetSecondaryMonitor(modName); - } -#endif - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. public void Dispose() { @@ -303,7 +286,7 @@ namespace StardewModdingAPI { PrintErrorAndExit( "Oops! SMAPI can't find the game. " - + (Assembly.GetCallingAssembly().Location?.Contains(Path.Combine("internal", "Windows")) == true || Assembly.GetCallingAssembly().Location?.Contains(Path.Combine("internal", "Mono")) == true + + (Assembly.GetCallingAssembly().Location.Contains(Path.Combine("internal", "Windows")) || Assembly.GetCallingAssembly().Location.Contains(Path.Combine("internal", "Mono")) ? "It looks like you're running SMAPI from the download package, but you need to run the installed version instead. " : "Make sure you're running StardewModdingAPI.exe in your game folder. " ) @@ -333,19 +316,6 @@ namespace StardewModdingAPI this.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry); this.CommandManager = new CommandManager(); -#if SMAPI_1_x - // inject compatibility shims -#pragma warning disable 618 - Command.Shim(this.CommandManager, this.DeprecationManager, this.ModRegistry); - Config.Shim(this.DeprecationManager); - Log.Shim(this.DeprecationManager, this.GetSecondaryMonitor("legacy mod"), this.ModRegistry); - Mod.Shim(this.DeprecationManager); - GameEvents.Shim(this.DeprecationManager); - PlayerEvents.Shim(this.DeprecationManager); - TimeEvents.Shim(this.DeprecationManager); -#pragma warning restore 618 -#endif - // redirect direct console output { Monitor monitor = this.GetSecondaryMonitor("Console.Out"); @@ -357,9 +327,7 @@ namespace StardewModdingAPI if (this.Settings.DeveloperMode) { this.Monitor.ShowTraceInConsole = true; -#if !SMAPI_1_x this.Monitor.ShowFullStampInConsole = true; -#endif this.Monitor.Log($"You configured SMAPI to run in developer mode. The console may be much more verbose. You can disable developer mode by installing the non-developer version of SMAPI, or by editing {Constants.ApiConfigPath}.", LogLevel.Info); } if (!this.Settings.CheckForUpdates) @@ -375,67 +343,18 @@ namespace StardewModdingAPI // load mods { -#if SMAPI_1_x - this.Monitor.Log("Loading mod metadata..."); -#else this.Monitor.Log("Loading mod metadata...", LogLevel.Trace); -#endif ModResolver resolver = new ModResolver(); // load manifests IModMetadata[] mods = resolver.ReadManifests(Constants.ModPath, new JsonHelper(), this.Settings.ModCompatibility, this.Settings.DisabledMods).ToArray(); resolver.ValidateManifests(mods, Constants.ApiVersion); - // check for deprecated metadata -#if SMAPI_1_x - IList deprecationWarnings = new List(); - foreach (IModMetadata mod in mods.Where(m => m.Status != ModMetadataStatus.Failed)) - { - // missing fields that will be required in SMAPI 2.0 - { - List missingFields = new List(3); - - if (string.IsNullOrWhiteSpace(mod.Manifest.Name)) - missingFields.Add(nameof(IManifest.Name)); - if (mod.Manifest.Version == null || mod.Manifest.Version.ToString() == "0.0") - missingFields.Add(nameof(IManifest.Version)); - if (string.IsNullOrWhiteSpace(mod.Manifest.UniqueID)) - missingFields.Add(nameof(IManifest.UniqueID)); - - if (missingFields.Any()) - deprecationWarnings.Add(() => this.Monitor.Log($"{mod.DisplayName} is missing some manifest fields ({string.Join(", ", missingFields)}) which will be required in an upcoming SMAPI version.", LogLevel.Warn)); - } - - // per-save directories - if ((mod.Manifest as Manifest)?.PerSaveConfigs == true) - { - deprecationWarnings.Add(() => this.DeprecationManager.Warn(mod.DisplayName, $"{nameof(Manifest)}.{nameof(Manifest.PerSaveConfigs)}", "1.0", DeprecationLevel.PendingRemoval)); - try - { - string psDir = Path.Combine(mod.DirectoryPath, "psconfigs"); - Directory.CreateDirectory(psDir); - if (!Directory.Exists(psDir)) - mod.SetStatus(ModMetadataStatus.Failed, "it requires per-save configuration files ('psconfigs') which couldn't be created for some reason."); - } - catch (Exception ex) - { - mod.SetStatus(ModMetadataStatus.Failed, $"it requires per-save configuration files ('psconfigs') which couldn't be created: {ex.GetLogSummary()}"); - } - } - } -#endif - // process dependencies mods = resolver.ProcessDependencies(mods).ToArray(); // load mods -#if SMAPI_1_x - this.LoadMods(mods, new JsonHelper(), this.ContentManager, deprecationWarnings); - foreach (Action warning in deprecationWarnings) - warning(); -#else this.LoadMods(mods, new JsonHelper(), this.ContentManager); -#endif // check for updates this.CheckForUpdatesAsync(mods); @@ -472,9 +391,6 @@ namespace StardewModdingAPI private void RunConsoleLoop() { // prepare console -#if SMAPI_1_x - this.Monitor.Log("Starting console..."); -#endif this.Monitor.Log("Type 'help' for help, or 'help ' for a command's usage", LogLevel.Info); this.CommandManager.Add("SMAPI", "help", "Lists command documentation.\n\nUsage: help\nLists all available commands.\n\nUsage: help \n- cmd: The name of a command whose documentation to display.", this.HandleCommand); this.CommandManager.Add("SMAPI", "reload_i18n", "Reloads translation files for all mods.\n\nUsage: reload_i18n", this.HandleCommand); @@ -600,7 +516,6 @@ namespace StardewModdingAPI } // fetch mod versions -#if !SMAPI_1_x try { // prepare update-check data @@ -649,14 +564,11 @@ namespace StardewModdingAPI { this.Monitor.Log($"Couldn't check for new mod versions:\n{ex.GetLogSummary()}", LogLevel.Trace); } -#endif // output if (updates.Any()) { -#if !SMAPI_1_x this.Monitor.Newline(); -#endif // print intro string intro = ""; @@ -694,18 +606,10 @@ namespace StardewModdingAPI /// The mods to load. /// The JSON helper with which to read mods' JSON files. /// The content manager to use for mod content. -#if SMAPI_1_x - /// A list to populate with any deprecation warnings. - private void LoadMods(IModMetadata[] mods, JsonHelper jsonHelper, SContentManager contentManager, IList deprecationWarnings) -#else private void LoadMods(IModMetadata[] mods, JsonHelper jsonHelper, SContentManager contentManager) -#endif { -#if SMAPI_1_x - this.Monitor.Log("Loading mods..."); -#else this.Monitor.Log("Loading mods...", LogLevel.Trace); -#endif + // load mod assemblies IDictionary skippedMods = new Dictionary(); { @@ -740,11 +644,7 @@ namespace StardewModdingAPI } catch (IncompatibleInstructionException ex) { -#if SMAPI_1_x - TrackSkip(metadata, $"it's not compatible with the latest version of the game or SMAPI (detected {ex.NounPhrase}). Please check for a newer version of the mod."); -#else TrackSkip(metadata, $"it's no longer compatible (detected {ex.NounPhrase}). Please check for a newer version of the mod."); -#endif continue; } catch (SAssemblyLoadFailedException ex) @@ -791,16 +691,6 @@ namespace StardewModdingAPI continue; } -#if SMAPI_1_x - // prevent mods from using SMAPI 2.0 content interception before release - // ReSharper disable SuspiciousTypeConversion.Global - if (mod is IAssetEditor || mod is IAssetLoader) - { - TrackSkip(metadata, $"its entry class implements {nameof(IAssetEditor)} or {nameof(IAssetLoader)}. These are part of a prototype API that isn't available for mods to use yet."); - } - // ReSharper restore SuspiciousTypeConversion.Global -#endif - // inject data { IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName); @@ -813,9 +703,6 @@ namespace StardewModdingAPI mod.ModManifest = manifest; mod.Helper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, translationHelper); mod.Monitor = monitor; -#if SMAPI_1_x - mod.PathOnDisk = metadata.DirectoryPath; -#endif } // track mod @@ -831,9 +718,7 @@ namespace StardewModdingAPI IModMetadata[] loadedMods = this.ModRegistry.GetMods().ToArray(); // log skipped mods -#if !SMAPI_1_x this.Monitor.Newline(); -#endif if (skippedMods.Any()) { this.Monitor.Log($"Skipped {skippedMods.Count} mods:", LogLevel.Error); @@ -847,9 +732,7 @@ namespace StardewModdingAPI else this.Monitor.Log($" {mod.DisplayName} because {reason}", LogLevel.Error); } -#if !SMAPI_1_x this.Monitor.Newline(); -#endif } // log loaded mods @@ -864,9 +747,7 @@ namespace StardewModdingAPI LogLevel.Info ); } -#if !SMAPI_1_x this.Monitor.Newline(); -#endif // initialise translations this.ReloadTranslations(); @@ -886,16 +767,8 @@ namespace StardewModdingAPI { IMod mod = metadata.Mod; mod.Entry(mod.Helper); -#if SMAPI_1_x - (mod as Mod)?.Entry(); // deprecated since 1.0 - - // raise deprecation warning for old Entry() methods - if (this.DeprecationManager.IsVirtualMethodImplemented(mod.GetType(), typeof(Mod), nameof(Mod.Entry), new[] { typeof(object[]) })) - deprecationWarnings.Add(() => this.DeprecationManager.Warn(metadata.DisplayName, $"{nameof(Mod)}.{nameof(Mod.Entry)}(object[]) instead of {nameof(Mod)}.{nameof(Mod.Entry)}({nameof(IModHelper)})", "1.0", DeprecationLevel.PendingRemoval)); -#else if (!this.DeprecationManager.IsVirtualMethodImplemented(mod.GetType(), typeof(Mod), nameof(Mod.Entry), new[] { typeof(IModHelper) })) this.Monitor.Log($"{metadata.DisplayName} doesn't implement Entry() and may not work correctly.", LogLevel.Error); -#endif } catch (Exception ex) { @@ -987,10 +860,6 @@ namespace StardewModdingAPI } else { -#if SMAPI_1_x - this.Monitor.Log("The following commands are registered: " + string.Join(", ", this.CommandManager.GetAll().Select(p => p.Name)) + ".", LogLevel.Info); - this.Monitor.Log("For more information about a command, type 'help command_name'.", LogLevel.Info); -#else string message = "The following commands are registered:\n"; IGrouping[] groups = (from command in this.CommandManager.GetAll() orderby command.ModName, command.Name group command.Name by command.ModName).ToArray(); foreach (var group in groups) @@ -1002,7 +871,6 @@ namespace StardewModdingAPI message += "For more information about a command, type 'help command_name'."; this.Monitor.Log(message, LogLevel.Info); -#endif } break; @@ -1051,9 +919,7 @@ namespace StardewModdingAPI { WriteToConsole = this.Monitor.WriteToConsole, ShowTraceInConsole = this.Settings.DeveloperMode, -#if !SMAPI_1_x ShowFullStampInConsole = this.Settings.DeveloperMode -#endif }; } diff --git a/src/StardewModdingAPI/SemanticVersion.cs b/src/StardewModdingAPI/SemanticVersion.cs index e448eae1..1b99dae6 100644 --- a/src/StardewModdingAPI/SemanticVersion.cs +++ b/src/StardewModdingAPI/SemanticVersion.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Text.RegularExpressions; using Newtonsoft.Json; @@ -179,7 +179,6 @@ namespace StardewModdingAPI return this.IsBetween(new SemanticVersion(min), new SemanticVersion(max)); } -#if !SMAPI_1_x /// Indicates whether the current object is equal to another object of the same type. /// true if the current object is equal to the parameter; otherwise, false. /// An object to compare with this object. @@ -187,7 +186,6 @@ namespace StardewModdingAPI { return other != null && this.CompareTo(other) == 0; } -#endif /// Get a string representation of the version. public override string ToString() diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 07e98674..685bef1e 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -90,7 +90,6 @@ Properties\GlobalAssemblyInfo.cs - @@ -120,10 +119,8 @@ - - @@ -131,19 +128,15 @@ - - - - @@ -222,7 +215,6 @@ - diff --git a/src/StardewModdingAPI/Utilities/SButton.cs b/src/StardewModdingAPI/Utilities/SButton.cs index 33058a64..fa5ae648 100644 --- a/src/StardewModdingAPI/Utilities/SButton.cs +++ b/src/StardewModdingAPI/Utilities/SButton.cs @@ -6,12 +6,7 @@ namespace StardewModdingAPI.Utilities { /// A unified button constant which includes all controller, keyboard, and mouse buttons. /// Derived from , , and . -#if SMAPI_1_x - internal -#else - public -#endif - enum SButton + public enum SButton { /// No valid key. None = 0, @@ -594,12 +589,7 @@ namespace StardewModdingAPI.Utilities } /// Provides extension methods for . -#if SMAPI_1_x - internal -#else - public -#endif - static class SButtonExtensions + public static class SButtonExtensions { /********* ** Accessors diff --git a/src/StardewModdingAPI/Utilities/SDate.cs b/src/StardewModdingAPI/Utilities/SDate.cs index 5073259d..326d7fc7 100644 --- a/src/StardewModdingAPI/Utilities/SDate.cs +++ b/src/StardewModdingAPI/Utilities/SDate.cs @@ -32,10 +32,8 @@ namespace StardewModdingAPI.Utilities /// The year. public int Year { get; } -#if !SMAPI_1_x /// The day of week. public DayOfWeek DayOfWeek { get; } -#endif /********* @@ -69,9 +67,7 @@ namespace StardewModdingAPI.Utilities this.Day = day; this.Season = season; this.Year = year; -#if !SMAPI_1_x this.DayOfWeek = this.GetDayOfWeek(); -#endif } /// Get the current in-game date. diff --git a/src/TrainerMod/Framework/Commands/Saves/LoadCommand.cs b/src/TrainerMod/Framework/Commands/Saves/LoadCommand.cs deleted file mode 100644 index ad79b4af..00000000 --- a/src/TrainerMod/Framework/Commands/Saves/LoadCommand.cs +++ /dev/null @@ -1,30 +0,0 @@ -#if SMAPI_1_x -using StardewModdingAPI; -using StardewValley; -using StardewValley.Menus; - -namespace TrainerMod.Framework.Commands.Saves -{ - /// A command which shows the load screen. - internal class LoadCommand : TrainerCommand - { - /********* - ** Public methods - *********/ - /// Construct an instance. - public LoadCommand() - : base("load", "Shows the load screen.") { } - - /// Handle the command. - /// Writes messages to the console and log file. - /// The command name. - /// The command arguments. - public override void Handle(IMonitor monitor, string command, ArgumentParser args) - { - monitor.Log("Triggering load menu...", LogLevel.Info); - Game1.hasLoadedGame = false; - Game1.activeClickableMenu = new LoadGameMenu(); - } - } -} -#endif \ No newline at end of file diff --git a/src/TrainerMod/Framework/Commands/Saves/SaveCommand.cs b/src/TrainerMod/Framework/Commands/Saves/SaveCommand.cs deleted file mode 100644 index ea2bd6a8..00000000 --- a/src/TrainerMod/Framework/Commands/Saves/SaveCommand.cs +++ /dev/null @@ -1,29 +0,0 @@ -#if SMAPI_1_x -using StardewModdingAPI; -using StardewValley; - -namespace TrainerMod.Framework.Commands.Saves -{ - /// A command which saves the game. - internal class SaveCommand : TrainerCommand - { - /********* - ** Public methods - *********/ - /// Construct an instance. - public SaveCommand() - : base("save", "Saves the game? Doesn't seem to work.") { } - - - /// Handle the command. - /// Writes messages to the console and log file. - /// The command name. - /// The command arguments. - public override void Handle(IMonitor monitor, string command, ArgumentParser args) - { - monitor.Log("Saving the game...", LogLevel.Info); - SaveGame.Save(); - } - } -} -#endif \ No newline at end of file diff --git a/src/TrainerMod/TrainerMod.csproj b/src/TrainerMod/TrainerMod.csproj index 73b40050..383e8c28 100644 --- a/src/TrainerMod/TrainerMod.csproj +++ b/src/TrainerMod/TrainerMod.csproj @@ -69,8 +69,6 @@ - - From 8bcc80a33d42f5c6f044533bb151d75901de912f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 23 Sep 2017 21:35:07 -0400 Subject: [PATCH 095/186] rename mod compatibility type for broader use (#361) --- src/StardewModdingAPI.Tests/Core/ModResolverTests.cs | 2 +- src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs | 2 +- src/StardewModdingAPI/Framework/Models/ModCompatibility.cs | 6 +++--- .../Models/{ModCompatibilityType.cs => ModStatus.cs} | 7 +++++-- src/StardewModdingAPI/Program.cs | 2 +- src/StardewModdingAPI/StardewModdingAPI.csproj | 2 +- 6 files changed, 12 insertions(+), 9 deletions(-) rename src/StardewModdingAPI/Framework/Models/{ModCompatibilityType.cs => ModStatus.cs} (66%) diff --git a/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs b/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs index 7a81c68c..bc3a0c8c 100644 --- a/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs +++ b/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs @@ -141,7 +141,7 @@ namespace StardewModdingAPI.Tests.Core { // arrange Mock mock = this.GetMetadata("Mod A", new string[0], allowStatusChange: true); - this.SetupMetadataForValidation(mock, new ModCompatibility { Compatibility = ModCompatibilityType.AssumeBroken, UpperVersion = new SemanticVersion("1.0"), UpdateUrls = new[] { "http://example.org" } }); + this.SetupMetadataForValidation(mock, new ModCompatibility { Status = ModStatus.AssumeBroken, UpperVersion = new SemanticVersion("1.0"), UpdateUrls = new[] { "http://example.org" } }); // act new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0")); diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs index 87b6a99c..6a971c15 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs @@ -107,7 +107,7 @@ namespace StardewModdingAPI.Framework.ModLoading // validate compatibility { ModCompatibility compatibility = mod.Compatibility; - if (compatibility?.Compatibility == ModCompatibilityType.AssumeBroken) + if (compatibility?.Status == ModStatus.AssumeBroken) { string reasonPhrase = compatibility.ReasonPhrase ?? "it's no longer compatible"; string error = $"{reasonPhrase}. Please check for a "; diff --git a/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs b/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs index d3a9c533..7489a306 100644 --- a/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs +++ b/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; using StardewModdingAPI.Framework.Serialisation; namespace StardewModdingAPI.Framework.Models @@ -34,7 +34,7 @@ namespace StardewModdingAPI.Framework.Models /// "this version is incompatible with the latest version of the game" public string ReasonPhrase { get; set; } - /// Indicates how SMAPI should consider the mod. - public ModCompatibilityType Compatibility { get; set; } = ModCompatibilityType.AssumeBroken; + /// Indicates how SMAPI should treat the mod. + public ModStatus Status { get; set; } = ModStatus.AssumeBroken; } } diff --git a/src/StardewModdingAPI/Framework/Models/ModCompatibilityType.cs b/src/StardewModdingAPI/Framework/Models/ModStatus.cs similarity index 66% rename from src/StardewModdingAPI/Framework/Models/ModCompatibilityType.cs rename to src/StardewModdingAPI/Framework/Models/ModStatus.cs index 35edec5e..4ab0b790 100644 --- a/src/StardewModdingAPI/Framework/Models/ModCompatibilityType.cs +++ b/src/StardewModdingAPI/Framework/Models/ModStatus.cs @@ -1,8 +1,11 @@ namespace StardewModdingAPI.Framework.Models { - /// Indicates how SMAPI should consider a mod. - internal enum ModCompatibilityType + /// Indicates how SMAPI should treat a mod. + internal enum ModStatus { + /// Don't override the status. + None = 0, + /// Assume the mod is not compatible, even if SMAPI doesn't detect any incompatible code. AssumeBroken = 0, diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 8f32b9eb..234572a6 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -640,7 +640,7 @@ namespace StardewModdingAPI Assembly modAssembly; try { - modAssembly = modAssemblyLoader.Load(metadata, assemblyPath, assumeCompatible: metadata.Compatibility?.Compatibility == ModCompatibilityType.AssumeCompatible); + modAssembly = modAssemblyLoader.Load(metadata, assemblyPath, assumeCompatible: metadata.Compatibility?.Status == ModStatus.AssumeCompatible); } catch (IncompatibleInstructionException ex) { diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 685bef1e..46f2ffb1 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -166,7 +166,7 @@ - + From a89dbce8549abee867d0af65ac62155bb485a911 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 23 Sep 2017 21:48:53 -0400 Subject: [PATCH 096/186] unify disabled-mod and compatibility lists (#361) --- .../Core/ModResolverTests.cs | 6 +- .../Framework/ModLoading/ModResolver.cs | 38 +++++----- .../Framework/Models/DisabledMod.cs | 22 ------ .../Framework/Models/ModStatus.cs | 9 ++- .../Framework/Models/SConfig.cs | 3 - src/StardewModdingAPI/Program.cs | 2 +- .../StardewModdingAPI.config.json | 76 +++++++++---------- .../StardewModdingAPI.csproj | 1 - 8 files changed, 67 insertions(+), 90 deletions(-) delete mode 100644 src/StardewModdingAPI/Framework/Models/DisabledMod.cs diff --git a/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs b/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs index bc3a0c8c..198ce190 100644 --- a/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs +++ b/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs @@ -30,7 +30,7 @@ namespace StardewModdingAPI.Tests.Core Directory.CreateDirectory(rootFolder); // act - IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModCompatibility[0], new DisabledMod[0]).ToArray(); + IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModCompatibility[0]).ToArray(); // assert Assert.AreEqual(0, mods.Length, 0, $"Expected to find zero manifests, found {mods.Length} instead."); @@ -45,7 +45,7 @@ namespace StardewModdingAPI.Tests.Core Directory.CreateDirectory(modFolder); // act - IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModCompatibility[0], new DisabledMod[0]).ToArray(); + IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModCompatibility[0]).ToArray(); IModMetadata mod = mods.FirstOrDefault(); // assert @@ -84,7 +84,7 @@ namespace StardewModdingAPI.Tests.Core File.WriteAllText(filename, JsonConvert.SerializeObject(original)); // act - IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModCompatibility[0], new DisabledMod[0]).ToArray(); + IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModCompatibility[0]).ToArray(); IModMetadata mod = mods.FirstOrDefault(); // assert diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs index 6a971c15..02fd85ea 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs @@ -18,12 +18,10 @@ namespace StardewModdingAPI.Framework.ModLoading /// The root path to search for mods. /// The JSON helper with which to read manifests. /// Metadata about mods that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code. - /// Metadata about mods that SMAPI should consider obsolete and not load. /// Returns the manifests by relative folder. - public IEnumerable ReadManifests(string rootPath, JsonHelper jsonHelper, IEnumerable compatibilityRecords, IEnumerable disabledMods) + public IEnumerable ReadManifests(string rootPath, JsonHelper jsonHelper, IEnumerable compatibilityRecords) { compatibilityRecords = compatibilityRecords.ToArray(); - disabledMods = disabledMods.ToArray(); foreach (DirectoryInfo modDir in this.GetModFolders(rootPath)) { @@ -62,18 +60,13 @@ namespace StardewModdingAPI.Framework.ModLoading // get unique key for lookups string key = !string.IsNullOrWhiteSpace(manifest.UniqueID) ? manifest.UniqueID : manifest.EntryDll; - // check if mod should be disabled - DisabledMod disabledMod = disabledMods.FirstOrDefault(mod => mod.ID.Contains(key, StringComparer.InvariantCultureIgnoreCase)); - if (disabledMod != null) - error = $"it's obsolete: {disabledMod.ReasonPhrase}"; - // get compatibility record compatibility = ( from mod in compatibilityRecords where mod.ID.Any(p => p.Matches(key, manifest)) && (mod.LowerVersion == null || !manifest.Version.IsOlderThan(mod.LowerVersion)) - && !manifest.Version.IsNewerThan(mod.UpperVersion) + && (mod.UpperVersion == null || !manifest.Version.IsNewerThan(mod.UpperVersion)) select mod ).FirstOrDefault(); } @@ -107,18 +100,25 @@ namespace StardewModdingAPI.Framework.ModLoading // validate compatibility { ModCompatibility compatibility = mod.Compatibility; - if (compatibility?.Status == ModStatus.AssumeBroken) + switch (compatibility?.Status) { - string reasonPhrase = compatibility.ReasonPhrase ?? "it's no longer compatible"; - string error = $"{reasonPhrase}. Please check for a "; - if (mod.Manifest.Version.Equals(compatibility.UpperVersion) && compatibility.UpperVersionLabel == null) - error += "newer version"; - else - error += $"version newer than {compatibility.UpperVersionLabel ?? compatibility.UpperVersion.ToString()}"; - error += " at " + string.Join(" or ", compatibility.UpdateUrls); + case ModStatus.Obsolete: + mod.SetStatus(ModMetadataStatus.Failed, $"it's obsolete: {compatibility.ReasonPhrase}"); + continue; - mod.SetStatus(ModMetadataStatus.Failed, error); - continue; + case ModStatus.AssumeBroken: + { + string reasonPhrase = compatibility.ReasonPhrase ?? "it's no longer compatible"; + string error = $"{reasonPhrase}. Please check for a "; + if (mod.Manifest.Version.Equals(compatibility.UpperVersion) && compatibility.UpperVersionLabel == null) + error += "newer version"; + else + error += $"version newer than {compatibility.UpperVersionLabel ?? compatibility.UpperVersion.ToString()}"; + error += " at " + string.Join(" or ", compatibility.UpdateUrls); + + mod.SetStatus(ModMetadataStatus.Failed, error); + continue; + } } } diff --git a/src/StardewModdingAPI/Framework/Models/DisabledMod.cs b/src/StardewModdingAPI/Framework/Models/DisabledMod.cs deleted file mode 100644 index 170fa760..00000000 --- a/src/StardewModdingAPI/Framework/Models/DisabledMod.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace StardewModdingAPI.Framework.Models -{ - /// Metadata about for a mod that should never be loaded. - internal class DisabledMod - { - /********* - ** Accessors - *********/ - /**** - ** From config - ****/ - /// The unique mod IDs. - public string[] ID { get; set; } - - /// The mod name. - public string Name { get; set; } - - /// The reason phrase to show in the warning, or null to use the default value. - /// "this mod is no longer supported or used" - public string ReasonPhrase { get; set; } - } -} diff --git a/src/StardewModdingAPI/Framework/Models/ModStatus.cs b/src/StardewModdingAPI/Framework/Models/ModStatus.cs index 4ab0b790..343ccb7e 100644 --- a/src/StardewModdingAPI/Framework/Models/ModStatus.cs +++ b/src/StardewModdingAPI/Framework/Models/ModStatus.cs @@ -4,12 +4,15 @@ namespace StardewModdingAPI.Framework.Models internal enum ModStatus { /// Don't override the status. - None = 0, + None, + + /// The mod is obsolete and shouldn't be used, regardless of version. + Obsolete, /// Assume the mod is not compatible, even if SMAPI doesn't detect any incompatible code. - AssumeBroken = 0, + AssumeBroken, /// Assume the mod is compatible, even if SMAPI detects incompatible code. - AssumeCompatible = 1 + AssumeCompatible } } diff --git a/src/StardewModdingAPI/Framework/Models/SConfig.cs b/src/StardewModdingAPI/Framework/Models/SConfig.cs index 36799400..720d4a6d 100644 --- a/src/StardewModdingAPI/Framework/Models/SConfig.cs +++ b/src/StardewModdingAPI/Framework/Models/SConfig.cs @@ -23,8 +23,5 @@ namespace StardewModdingAPI.Framework.Models /// A list of mod versions which should be considered compatible or incompatible regardless of whether SMAPI detects incompatible code. public ModCompatibility[] ModCompatibility { get; set; } - - /// A list of mods which should be considered obsolete and not loaded. - public DisabledMod[] DisabledMods { get; set; } } } diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 234572a6..b22fb02a 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -347,7 +347,7 @@ namespace StardewModdingAPI ModResolver resolver = new ModResolver(); // load manifests - IModMetadata[] mods = resolver.ReadManifests(Constants.ModPath, new JsonHelper(), this.Settings.ModCompatibility, this.Settings.DisabledMods).ToArray(); + IModMetadata[] mods = resolver.ReadManifests(Constants.ModPath, new JsonHelper(), this.Settings.ModCompatibility).ToArray(); resolver.ValidateManifests(mods, Constants.ApiVersion); // process dependencies diff --git a/src/StardewModdingAPI/StardewModdingAPI.config.json b/src/StardewModdingAPI/StardewModdingAPI.config.json index e5c42391..7c20a9d9 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.config.json +++ b/src/StardewModdingAPI/StardewModdingAPI.config.json @@ -28,7 +28,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha /** * The base URL for SMAPI's web API, used to perform update checks. - * Note: the protocol will be changed to http:// on Linux/Mac due to OpenSSL issues with the game's bundled Mono. + * 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", @@ -37,43 +38,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha */ "VerboseLogging": false, - /** - * A list of mods SMAPI should consider obsolete and not load. Changing this field is not - * recommended and may destabilise your game. - */ - "DisabledMods": [ - { - "Name": "Animal Mood Fix", - "ID": [ "GPeters-AnimalMoodFix" ], - "ReasonPhrase": "the animal mood bugs were fixed in Stardew Valley 1.2." - }, - { - "Name": "Colored Chests", - "ID": [ "4befde5c-731c-4853-8e4b-c5cdf946805f" ], - "ReasonPhrase": "colored chests were added in Stardew Valley 1.1." - }, - { - "Name": "Modder Serialization Utility", - "ID": [ "SerializerUtils-0-1" ], - "ReasonPhrase": "it's no longer maintained or used." - }, - { - "Name": "No Debug Mode", - "ID": [ "NoDebugMode" ], - "ReasonPhrase": "debug mode was removed in SMAPI 1.0." - }, - { - "Name": "StarDustCore", - "ID": [ "StarDustCore" ], - "ReasonPhrase": "it was only used by earlier versions of Save Anywhere, and is no longer used or maintained." - }, - { - "Name": "XmlSerializerRetool", - "ID": [ "XmlSerializerRetool.dll" ], - "ReasonPhrase": "it's no longer maintained or used." - } - ], - /** * A list of mod versions SMAPI should consider compatible or broken regardless of whether it * detects incompatible code. The default for each record is to assume broken; to force SMAPI to @@ -117,6 +81,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/439" ], "Notes": "Broke in SDV 1.2." }, + { + "Name": "Animal Mood Fix", + "ID": [ "GPeters-AnimalMoodFix" ], + "Status": "Obsolete", + "ReasonPhrase": "the animal mood bugs were fixed in Stardew Valley 1.2." + }, { "Name": "Animal Sitter", "ID": [ "AnimalSitter.dll" ], @@ -243,6 +213,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1169", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, + { + "Name": "Colored Chests", + "ID": [ "4befde5c-731c-4853-8e4b-c5cdf946805f" ], + "Status": "Obsolete", + "ReasonPhrase": "colored chests were added in Stardew Valley 1.1." + }, { "Name": "Combat with Farm Implements", "ID": [ "SPDFarmingImplementsInCombat" ], @@ -523,6 +499,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "UpdateUrls": [ "http://community.playstarbound.com/resources/message-box-api.4296", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, + { + "Name": "Modder Serialization Utility", + "ID": [ "SerializerUtils-0-1" ], + "Status": "Obsolete", + "ReasonPhrase": "it's no longer maintained or used." + }, { "Name": "More Artifact Spots", "ID": [ "451" ], @@ -572,6 +554,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/433", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, + { + "Name": "No Debug Mode", + "ID": [ "NoDebugMode" ], + "Status": "Obsolete", + "ReasonPhrase": "debug mode was removed in SMAPI 1.0." + }, { "Name": "NoSoilDecay", "ID": [ "289dee03-5f38-4d8e-8ffc-e440198e8610" ], @@ -853,6 +841,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/425", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, + { + "Name": "StarDustCore", + "ID": [ "StarDustCore" ], + "Status": "Obsolete", + "ReasonPhrase": "it was only used by earlier versions of Save Anywhere, and is no longer used or maintained." + }, { "Name": "StashItemsToChest", "ID": [ "BlueMod_StashItemsToChest" ], @@ -938,6 +932,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "UpdateUrls": [ "http://community.playstarbound.com/threads/115384", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SDV 1.1 or 1.11." }, + { + "Name": "XmlSerializerRetool", + "ID": [ "XmlSerializerRetool.dll" ], + "Status": "Obsolete", + "ReasonPhrase": "it's no longer maintained or used." + }, { "Name": "Xnb Loader", "ID": [ "Entoarox.XnbLoader" ], diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 46f2ffb1..818e7263 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -142,7 +142,6 @@ - From 9495cc0f493a11960aa23551e164018681d08f79 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 23 Sep 2017 22:07:29 -0400 Subject: [PATCH 097/186] rename mod compatibility records for broader use (#361) --- .../Core/ModResolverTests.cs | 28 ++++++++--------- .../Framework/IModMetadata.cs | 6 ++-- .../Framework/ModLoading/ModMetadata.cs | 12 ++++---- .../Framework/ModLoading/ModResolver.cs | 30 +++++++++---------- .../{ModCompatibilityID.cs => ModDataID.cs} | 12 ++++---- .../{ModCompatibility.cs => ModDataRecord.cs} | 6 ++-- .../Framework/Models/SConfig.cs | 4 +-- .../Serialisation/SFieldConverter.cs | 10 +++---- src/StardewModdingAPI/Program.cs | 4 +-- .../StardewModdingAPI.config.json | 2 +- .../StardewModdingAPI.csproj | 4 +-- 11 files changed, 59 insertions(+), 59 deletions(-) rename src/StardewModdingAPI/Framework/Models/{ModCompatibilityID.cs => ModDataID.cs} (88%) rename src/StardewModdingAPI/Framework/Models/{ModCompatibility.cs => ModDataRecord.cs} (86%) diff --git a/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs b/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs index 198ce190..4dbd1438 100644 --- a/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs +++ b/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -30,7 +30,7 @@ namespace StardewModdingAPI.Tests.Core Directory.CreateDirectory(rootFolder); // act - IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModCompatibility[0]).ToArray(); + IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModDataRecord[0]).ToArray(); // assert Assert.AreEqual(0, mods.Length, 0, $"Expected to find zero manifests, found {mods.Length} instead."); @@ -45,7 +45,7 @@ namespace StardewModdingAPI.Tests.Core Directory.CreateDirectory(modFolder); // act - IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModCompatibility[0]).ToArray(); + IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModDataRecord[0]).ToArray(); IModMetadata mod = mods.FirstOrDefault(); // assert @@ -84,13 +84,13 @@ namespace StardewModdingAPI.Tests.Core File.WriteAllText(filename, JsonConvert.SerializeObject(original)); // act - IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModCompatibility[0]).ToArray(); + IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModDataRecord[0]).ToArray(); IModMetadata mod = mods.FirstOrDefault(); // assert Assert.AreEqual(1, mods.Length, 0, "Expected to find one manifest."); Assert.IsNotNull(mod, "The loaded manifest shouldn't be null."); - Assert.AreEqual(null, mod.Compatibility, "The compatibility record should be null since we didn't provide one."); + Assert.AreEqual(null, mod.DataRecord, "The data record should be null since we didn't provide one."); Assert.AreEqual(modFolder, mod.DirectoryPath, "The directory path doesn't match."); Assert.AreEqual(ModMetadataStatus.Found, mod.Status, "The status doesn't match."); Assert.AreEqual(null, mod.Error, "The error should be null since parsing should have succeeded."); @@ -136,12 +136,12 @@ namespace StardewModdingAPI.Tests.Core mock.VerifyGet(p => p.Status, Times.Once, "The validation did not check the manifest status."); } - [Test(Description = "Assert that validation fails if the mod has 'assume broken' compatibility.")] - public void ValidateManifests_ModCompatibility_AssumeBroken_Fails() + [Test(Description = "Assert that validation fails if the mod has 'assume broken' status.")] + public void ValidateManifests_ModStatus_AssumeBroken_Fails() { // arrange Mock mock = this.GetMetadata("Mod A", new string[0], allowStatusChange: true); - this.SetupMetadataForValidation(mock, new ModCompatibility { Status = ModStatus.AssumeBroken, UpperVersion = new SemanticVersion("1.0"), UpdateUrls = new[] { "http://example.org" } }); + this.SetupMetadataForValidation(mock, new ModDataRecord { Status = ModStatus.AssumeBroken, UpperVersion = new SemanticVersion("1.0"), UpdateUrls = new[] { "http://example.org" } }); // act new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0")); @@ -211,7 +211,7 @@ namespace StardewModdingAPI.Tests.Core // arrange Mock mock = new Mock(MockBehavior.Strict); mock.Setup(p => p.Status).Returns(ModMetadataStatus.Found); - mock.Setup(p => p.Compatibility).Returns(() => null); + mock.Setup(p => p.DataRecord).Returns(() => null); mock.Setup(p => p.Manifest).Returns(manifest); mock.Setup(p => p.DirectoryPath).Returns(modFolder); @@ -523,7 +523,7 @@ namespace StardewModdingAPI.Tests.Core private Mock GetMetadata(IManifest manifest, bool allowStatusChange = false) { Mock mod = new Mock(MockBehavior.Strict); - mod.Setup(p => p.Compatibility).Returns(() => null); + mod.Setup(p => p.DataRecord).Returns(() => null); mod.Setup(p => p.Status).Returns(ModMetadataStatus.Found); mod.Setup(p => p.DisplayName).Returns(manifest.UniqueID); mod.Setup(p => p.Manifest).Returns(manifest); @@ -539,14 +539,14 @@ namespace StardewModdingAPI.Tests.Core /// Set up a mock mod metadata for . /// The mock mod metadata. - /// The compatibility record to set. - private void SetupMetadataForValidation(Mock mod, ModCompatibility compatibility = null) + /// The extra metadata about the mod from SMAPI's internal data (if any). + private void SetupMetadataForValidation(Mock mod, ModDataRecord modRecord = null) { mod.Setup(p => p.Status).Returns(ModMetadataStatus.Found); - mod.Setup(p => p.Compatibility).Returns(() => null); + mod.Setup(p => p.DataRecord).Returns(() => null); mod.Setup(p => p.Manifest).Returns(this.GetManifest()); mod.Setup(p => p.DirectoryPath).Returns(Path.GetTempPath()); - mod.Setup(p => p.Compatibility).Returns(compatibility); + mod.Setup(p => p.DataRecord).Returns(modRecord); } } } diff --git a/src/StardewModdingAPI/Framework/IModMetadata.cs b/src/StardewModdingAPI/Framework/IModMetadata.cs index 56ac25f1..c21734a7 100644 --- a/src/StardewModdingAPI/Framework/IModMetadata.cs +++ b/src/StardewModdingAPI/Framework/IModMetadata.cs @@ -1,4 +1,4 @@ -using StardewModdingAPI.Framework.Models; +using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.ModLoading; namespace StardewModdingAPI.Framework @@ -18,8 +18,8 @@ namespace StardewModdingAPI.Framework /// The mod manifest. IManifest Manifest { get; } - /// Optional metadata about a mod version that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code. - ModCompatibility Compatibility { get; } + /// >Metadata about the mod from SMAPI's internal data (if any). + ModDataRecord DataRecord { get; } /// The metadata resolution status. ModMetadataStatus Status { get; } diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs b/src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs index ab590e10..5055da75 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs @@ -1,4 +1,4 @@ -using StardewModdingAPI.Framework.Models; +using StardewModdingAPI.Framework.Models; namespace StardewModdingAPI.Framework.ModLoading { @@ -17,8 +17,8 @@ namespace StardewModdingAPI.Framework.ModLoading /// The mod manifest. public IManifest Manifest { get; } - /// Optional metadata about a mod version that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code. - public ModCompatibility Compatibility { get; } + /// Metadata about the mod from SMAPI's internal data (if any). + public ModDataRecord DataRecord { get; } /// The metadata resolution status. public ModMetadataStatus Status { get; private set; } @@ -37,13 +37,13 @@ namespace StardewModdingAPI.Framework.ModLoading /// The mod's display name. /// The mod's full directory path. /// The mod manifest. - /// Optional metadata about a mod version that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code. - public ModMetadata(string displayName, string directoryPath, IManifest manifest, ModCompatibility compatibility) + /// Metadata about the mod from SMAPI's internal data (if any). + public ModMetadata(string displayName, string directoryPath, IManifest manifest, ModDataRecord dataRecord) { this.DisplayName = displayName; this.DirectoryPath = directoryPath; this.Manifest = manifest; - this.Compatibility = compatibility; + this.DataRecord = dataRecord; } /// Set the mod status. diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs index 02fd85ea..2da10348 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs @@ -17,11 +17,11 @@ namespace StardewModdingAPI.Framework.ModLoading /// Get manifest metadata for each folder in the given root path. /// The root path to search for mods. /// The JSON helper with which to read manifests. - /// Metadata about mods that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code. + /// Metadata about mods from SMAPI's internal data. /// Returns the manifests by relative folder. - public IEnumerable ReadManifests(string rootPath, JsonHelper jsonHelper, IEnumerable compatibilityRecords) + public IEnumerable ReadManifests(string rootPath, JsonHelper jsonHelper, IEnumerable dataRecords) { - compatibilityRecords = compatibilityRecords.ToArray(); + dataRecords = dataRecords.ToArray(); foreach (DirectoryInfo modDir in this.GetModFolders(rootPath)) { @@ -54,15 +54,15 @@ namespace StardewModdingAPI.Framework.ModLoading } // validate metadata - ModCompatibility compatibility = null; + ModDataRecord dataRecord = null; if (manifest != null) { // get unique key for lookups string key = !string.IsNullOrWhiteSpace(manifest.UniqueID) ? manifest.UniqueID : manifest.EntryDll; - // get compatibility record - compatibility = ( - from mod in compatibilityRecords + // get data record + dataRecord = ( + from mod in dataRecords where mod.ID.Any(p => p.Matches(key, manifest)) && (mod.LowerVersion == null || !manifest.Version.IsOlderThan(mod.LowerVersion)) @@ -79,7 +79,7 @@ namespace StardewModdingAPI.Framework.ModLoading ? ModMetadataStatus.Found : ModMetadataStatus.Failed; - yield return new ModMetadata(displayName, modDir.FullName, manifest, compatibility).SetStatus(status, error); + yield return new ModMetadata(displayName, modDir.FullName, manifest, dataRecord).SetStatus(status, error); } } @@ -99,22 +99,22 @@ namespace StardewModdingAPI.Framework.ModLoading // validate compatibility { - ModCompatibility compatibility = mod.Compatibility; - switch (compatibility?.Status) + ModDataRecord dataRecord = mod.DataRecord; + switch (dataRecord?.Status) { case ModStatus.Obsolete: - mod.SetStatus(ModMetadataStatus.Failed, $"it's obsolete: {compatibility.ReasonPhrase}"); + mod.SetStatus(ModMetadataStatus.Failed, $"it's obsolete: {dataRecord.ReasonPhrase}"); continue; case ModStatus.AssumeBroken: { - string reasonPhrase = compatibility.ReasonPhrase ?? "it's no longer compatible"; + string reasonPhrase = dataRecord.ReasonPhrase ?? "it's no longer compatible"; string error = $"{reasonPhrase}. Please check for a "; - if (mod.Manifest.Version.Equals(compatibility.UpperVersion) && compatibility.UpperVersionLabel == null) + if (mod.Manifest.Version.Equals(dataRecord.UpperVersion) && dataRecord.UpperVersionLabel == null) error += "newer version"; else - error += $"version newer than {compatibility.UpperVersionLabel ?? compatibility.UpperVersion.ToString()}"; - error += " at " + string.Join(" or ", compatibility.UpdateUrls); + error += $"version newer than {dataRecord.UpperVersionLabel ?? dataRecord.UpperVersion.ToString()}"; + error += " at " + string.Join(" or ", dataRecord.UpdateUrls); mod.SetStatus(ModMetadataStatus.Failed, error); continue; diff --git a/src/StardewModdingAPI/Framework/Models/ModCompatibilityID.cs b/src/StardewModdingAPI/Framework/Models/ModDataID.cs similarity index 88% rename from src/StardewModdingAPI/Framework/Models/ModCompatibilityID.cs rename to src/StardewModdingAPI/Framework/Models/ModDataID.cs index 98e70116..5b45b507 100644 --- a/src/StardewModdingAPI/Framework/Models/ModCompatibilityID.cs +++ b/src/StardewModdingAPI/Framework/Models/ModDataID.cs @@ -1,10 +1,10 @@ -using System; +using System; using Newtonsoft.Json; namespace StardewModdingAPI.Framework.Models { - /// Uniquely identifies a mod for compatibility checks. - internal class ModCompatibilityID + /// Uniquely identifies a mod in SMAPI's internal data. + internal class ModDataID { /********* ** Accessors @@ -23,11 +23,11 @@ namespace StardewModdingAPI.Framework.Models ** Public methods *********/ /// Construct an instance. - public ModCompatibilityID() { } + public ModDataID() { } /// Construct an instance. - /// The mod ID or a JSON string matching the fields. - public ModCompatibilityID(string data) + /// The mod ID or a JSON string matching the fields. + public ModDataID(string data) { // JSON can be stuffed into the ID string as a convenience hack to keep JSON mod lists // formatted readably. The tradeoff is that the format is a bit more magical, but that's diff --git a/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs b/src/StardewModdingAPI/Framework/Models/ModDataRecord.cs similarity index 86% rename from src/StardewModdingAPI/Framework/Models/ModCompatibility.cs rename to src/StardewModdingAPI/Framework/Models/ModDataRecord.cs index 7489a306..d40f2c78 100644 --- a/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs +++ b/src/StardewModdingAPI/Framework/Models/ModDataRecord.cs @@ -3,15 +3,15 @@ using StardewModdingAPI.Framework.Serialisation; namespace StardewModdingAPI.Framework.Models { - /// Metadata about a mod version that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code. - internal class ModCompatibility + /// Metadata about a mod from SMAPI's internal data. + internal class ModDataRecord { /********* ** Accessors *********/ /// The unique mod IDs. [JsonConverter(typeof(SFieldConverter))] - public ModCompatibilityID[] ID { get; set; } + public ModDataID[] ID { get; set; } /// The mod name. public string Name { get; set; } diff --git a/src/StardewModdingAPI/Framework/Models/SConfig.cs b/src/StardewModdingAPI/Framework/Models/SConfig.cs index 720d4a6d..401e1a3a 100644 --- a/src/StardewModdingAPI/Framework/Models/SConfig.cs +++ b/src/StardewModdingAPI/Framework/Models/SConfig.cs @@ -21,7 +21,7 @@ namespace StardewModdingAPI.Framework.Models /// Whether SMAPI should log more information about the game context. public bool VerboseLogging { get; set; } - /// A list of mod versions which should be considered compatible or incompatible regardless of whether SMAPI detects incompatible code. - public ModCompatibility[] ModCompatibility { get; set; } + /// Extra metadata about mods. + public ModDataRecord[] ModData { get; set; } } } diff --git a/src/StardewModdingAPI/Framework/Serialisation/SFieldConverter.cs b/src/StardewModdingAPI/Framework/Serialisation/SFieldConverter.cs index 5419896f..59cc1582 100644 --- a/src/StardewModdingAPI/Framework/Serialisation/SFieldConverter.cs +++ b/src/StardewModdingAPI/Framework/Serialisation/SFieldConverter.cs @@ -27,7 +27,7 @@ namespace StardewModdingAPI.Framework.Serialisation return objectType == typeof(ISemanticVersion) || objectType == typeof(IManifestDependency[]) - || objectType == typeof(ModCompatibilityID[]); + || objectType == typeof(ModDataID[]); } /// Reads the JSON representation of the object. @@ -83,14 +83,14 @@ namespace StardewModdingAPI.Framework.Serialisation } // mod compatibility ID - if (objectType == typeof(ModCompatibilityID[])) + if (objectType == typeof(ModDataID[])) { - List result = new List(); + List result = new List(); foreach (JToken child in JArray.Load(reader).Children()) { result.Add(child is JValue value - ? new ModCompatibilityID(value.Value()) - : child.ToObject() + ? new ModDataID(value.Value()) + : child.ToObject() ); } return result.ToArray(); diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index b22fb02a..63176dd1 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -347,7 +347,7 @@ namespace StardewModdingAPI ModResolver resolver = new ModResolver(); // load manifests - IModMetadata[] mods = resolver.ReadManifests(Constants.ModPath, new JsonHelper(), this.Settings.ModCompatibility).ToArray(); + IModMetadata[] mods = resolver.ReadManifests(Constants.ModPath, new JsonHelper(), this.Settings.ModData).ToArray(); resolver.ValidateManifests(mods, Constants.ApiVersion); // process dependencies @@ -640,7 +640,7 @@ namespace StardewModdingAPI Assembly modAssembly; try { - modAssembly = modAssemblyLoader.Load(metadata, assemblyPath, assumeCompatible: metadata.Compatibility?.Status == ModStatus.AssumeCompatible); + modAssembly = modAssemblyLoader.Load(metadata, assemblyPath, assumeCompatible: metadata.DataRecord?.Status == ModStatus.AssumeCompatible); } catch (IncompatibleInstructionException ex) { diff --git a/src/StardewModdingAPI/StardewModdingAPI.config.json b/src/StardewModdingAPI/StardewModdingAPI.config.json index 7c20a9d9..aa6df389 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.config.json +++ b/src/StardewModdingAPI/StardewModdingAPI.config.json @@ -44,7 +44,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha * load a mod regardless of compatibility checks, add a "Compatibility": "AssumeCompatible" field. * Changing this field is not recommended and may destabilise your game. */ - "ModCompatibility": [ + "ModData": [ { "Name": "AccessChestAnywhere", "ID": [ "AccessChestAnywhere" ], diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 818e7263..38064f98 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -142,7 +142,7 @@ - + @@ -194,7 +194,7 @@ - + From 33af789e2eb4da101cead531b77c29ecf4933549 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 23 Sep 2017 22:50:35 -0400 Subject: [PATCH 098/186] abstract mod IDs with multiple variants (#361) --- .../Framework/ModLoading/ModResolver.cs | 2 +- .../Framework/Models/ModDataID.cs | 76 +++-- .../Framework/Models/ModDataRecord.cs | 4 +- .../Serialisation/SFieldConverter.cs | 15 +- .../StardewModdingAPI.config.json | 274 +++++++++--------- 5 files changed, 196 insertions(+), 175 deletions(-) diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs index 2da10348..3674faec 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs @@ -64,7 +64,7 @@ namespace StardewModdingAPI.Framework.ModLoading dataRecord = ( from mod in dataRecords where - mod.ID.Any(p => p.Matches(key, manifest)) + mod.ID.Matches(key, manifest) && (mod.LowerVersion == null || !manifest.Version.IsOlderThan(mod.LowerVersion)) && (mod.UpperVersion == null || !manifest.Version.IsNewerThan(mod.UpperVersion)) select mod diff --git a/src/StardewModdingAPI/Framework/Models/ModDataID.cs b/src/StardewModdingAPI/Framework/Models/ModDataID.cs index 5b45b507..d19434fa 100644 --- a/src/StardewModdingAPI/Framework/Models/ModDataID.cs +++ b/src/StardewModdingAPI/Framework/Models/ModDataID.cs @@ -1,22 +1,28 @@ using System; +using System.Linq; using Newtonsoft.Json; namespace StardewModdingAPI.Framework.Models { /// Uniquely identifies a mod in SMAPI's internal data. + /// + /// This represents a custom format which uniquely identifies a mod across all versions, even + /// if its field values change or it doesn't specify a unique ID. This is mapped to a string + /// with the following format: + /// + /// 1. If the mod's identifier changed over time, multiple variants can be separated by the | + /// character. + /// 2. Each variant can take one of two forms: + /// - A simple string matching the mod's UniqueID value. + /// - A JSON structure containing any of three manifest fields (ID, Name, and Author) to match. + /// internal class ModDataID { /********* - ** Accessors + ** Properties *********/ - /// The unique mod ID. - public string ID { get; set; } - - /// The mod name to disambiguate non-unique IDs, or null to ignore the mod name. - public string Name { get; set; } - - /// The author name to disambiguate non-unique IDs, or null to ignore the author. - public string Author { get; set; } + /// The unique sets of field values which identify this mod. + private readonly FieldSnapshot[] Snapshots; /********* @@ -26,17 +32,18 @@ namespace StardewModdingAPI.Framework.Models public ModDataID() { } /// Construct an instance. - /// The mod ID or a JSON string matching the fields. + /// The mod identifier string (see remarks on ). public ModDataID(string data) { - // JSON can be stuffed into the ID string as a convenience hack to keep JSON mod lists - // formatted readably. The tradeoff is that the format is a bit more magical, but that's - // probably acceptable since players aren't meant to edit it. It's also fairly clear what - // the JSON strings do, if not necessarily how. - if (data.StartsWith("{")) - JsonConvert.PopulateObject(data, this); - else - this.ID = data; + this.Snapshots = + ( + from string part in data.Split('|') + let str = part.Trim() + select str.StartsWith("{") + ? JsonConvert.DeserializeObject(str) + : new FieldSnapshot { ID = str } + ) + .ToArray(); } /// Get whether this ID matches a given mod manifest. @@ -44,14 +51,35 @@ namespace StardewModdingAPI.Framework.Models /// The manifest to check. public bool Matches(string id, IManifest manifest) { - return - this.ID.Equals(id, StringComparison.InvariantCultureIgnoreCase) + return this.Snapshots.Any(snapshot => + snapshot.ID.Equals(id, StringComparison.InvariantCultureIgnoreCase) && ( - this.Author == null - || this.Author.Equals(manifest.Author, StringComparison.InvariantCultureIgnoreCase) - || (manifest.ExtraFields.ContainsKey("Authour") && this.Author.Equals(manifest.ExtraFields["Authour"].ToString(), StringComparison.InvariantCultureIgnoreCase)) + snapshot.Author == null + || snapshot.Author.Equals(manifest.Author, StringComparison.InvariantCultureIgnoreCase) + || (manifest.ExtraFields.ContainsKey("Authour") && snapshot.Author.Equals(manifest.ExtraFields["Authour"].ToString(), StringComparison.InvariantCultureIgnoreCase)) ) - && (this.Name == null || this.Name.Equals(manifest.Name, StringComparison.InvariantCultureIgnoreCase)); + && (snapshot.Name == null || snapshot.Name.Equals(manifest.Name, StringComparison.InvariantCultureIgnoreCase)) + ); + } + + + /********* + ** Private models + *********/ + /// A unique set of fields which identifies the mod. + private class FieldSnapshot + { + /********* + ** Accessors + *********/ + /// The unique mod ID. + public string ID { get; set; } + + /// The mod name, or null to ignore the mod name. + public string Name { get; set; } + + /// The author name, or null to ignore the author. + public string Author { get; set; } } } } diff --git a/src/StardewModdingAPI/Framework/Models/ModDataRecord.cs b/src/StardewModdingAPI/Framework/Models/ModDataRecord.cs index d40f2c78..8126022d 100644 --- a/src/StardewModdingAPI/Framework/Models/ModDataRecord.cs +++ b/src/StardewModdingAPI/Framework/Models/ModDataRecord.cs @@ -9,9 +9,9 @@ namespace StardewModdingAPI.Framework.Models /********* ** Accessors *********/ - /// The unique mod IDs. + /// The unique mod identifier. [JsonConverter(typeof(SFieldConverter))] - public ModDataID[] ID { get; set; } + public ModDataID ID { get; set; } /// The mod name. public string Name { get; set; } diff --git a/src/StardewModdingAPI/Framework/Serialisation/SFieldConverter.cs b/src/StardewModdingAPI/Framework/Serialisation/SFieldConverter.cs index 59cc1582..d71e138b 100644 --- a/src/StardewModdingAPI/Framework/Serialisation/SFieldConverter.cs +++ b/src/StardewModdingAPI/Framework/Serialisation/SFieldConverter.cs @@ -27,7 +27,7 @@ namespace StardewModdingAPI.Framework.Serialisation return objectType == typeof(ISemanticVersion) || objectType == typeof(IManifestDependency[]) - || objectType == typeof(ModDataID[]); + || objectType == typeof(ModDataID); } /// Reads the JSON representation of the object. @@ -83,17 +83,10 @@ namespace StardewModdingAPI.Framework.Serialisation } // mod compatibility ID - if (objectType == typeof(ModDataID[])) + if (objectType == typeof(ModDataID)) { - List result = new List(); - foreach (JToken child in JArray.Load(reader).Children()) - { - result.Add(child is JValue value - ? new ModDataID(value.Value()) - : child.ToObject() - ); - } - return result.ToArray(); + JToken token = JToken.Load(reader); + return new ModDataID(token.Value()); } // unknown diff --git a/src/StardewModdingAPI/StardewModdingAPI.config.json b/src/StardewModdingAPI/StardewModdingAPI.config.json index aa6df389..c93ba130 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.config.json +++ b/src/StardewModdingAPI/StardewModdingAPI.config.json @@ -47,14 +47,14 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ModData": [ { "Name": "AccessChestAnywhere", - "ID": [ "AccessChestAnywhere" ], + "ID": "AccessChestAnywhere", "UpperVersion": "1.1", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/257", "http://www.nexusmods.com/stardewvalley/mods/518" ], "Notes": "Broke in SDV 1.1." }, { "Name": "AdjustArtisanPrices", - "ID": [ "1e36d4ca-c7ef-4dfb-9927-d27a6c3c8bdc" ], + "ID": "1e36d4ca-c7ef-4dfb-9927-d27a6c3c8bdc", "UpperVersion": "0.0", "UpperVersionLabel": "0.01", "UpdateUrls": [ "http://community.playstarbound.com/resources/3532", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], @@ -62,514 +62,514 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, { "Name": "Advanced Location Loader", - "ID": [ "Entoarox.AdvancedLocationLoader" ], + "ID": "Entoarox.AdvancedLocationLoader", "UpperVersion": "1.2.10", "UpdateUrls": [ "http://community.playstarbound.com/resources/3619" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "AgingMod", - "ID": [ "skn.AgingMod" ], + "ID": "skn.AgingMod", "UpperVersion": "1.0", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1129", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Almighty Tool", - "ID": [ "AlmightyTool.dll" ], + "ID": "AlmightyTool.dll", "UpperVersion": "1.1.1", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/439" ], "Notes": "Broke in SDV 1.2." }, { "Name": "Animal Mood Fix", - "ID": [ "GPeters-AnimalMoodFix" ], + "ID": "GPeters-AnimalMoodFix", "Status": "Obsolete", "ReasonPhrase": "the animal mood bugs were fixed in Stardew Valley 1.2." }, { "Name": "Animal Sitter", - "ID": [ "AnimalSitter.dll" ], + "ID": "AnimalSitter.dll", "UpperVersion": "1.0.8", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/581", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "A Tapper's Dream", - "ID": [ "ddde5195-8f85-4061-90cc-0d4fd5459358" ], + "ID": "ddde5195-8f85-4061-90cc-0d4fd5459358", "UpperVersion": "1.4", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/260", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Better Sprinklers", - "ID": [ "SPDSprinklersMod", /*since 2.3*/ "Speeder.BetterSprinklers" ], + "ID": "SPDSprinklersMod | Speeder.BetterSprinklers", // changed in 2.3 "UpperVersion": "2.3.1-pathoschild-update", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/41", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SDV 1.2." }, { "Name": "Birthday Mail", - "ID": [ "005e02dc-d900-425c-9c68-1ff55c5a295d" ], + "ID": "005e02dc-d900-425c-9c68-1ff55c5a295d", "UpperVersion": "1.2.2", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/276", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SDV 1.2." }, { "Name": "Build Endurance", - "ID": [ "{'ID':'4be88c18-b6f3-49b0-ba96-f94b1a5be890', 'Name':'BuildEndurance'}" ], // disambiguate from other Alpha_Omegasis mods + "ID": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'BuildEndurance'}", // disambiguate from other Alpha_Omegasis mods "UpperVersion": "1.3", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/445", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Build Health", - "ID": [ "{'ID':'4be88c18-b6f3-49b0-ba96-f94b1a5be890', 'Name':'BuildHealth'}" ], // disambiguate from other Alpha_Omegasis mods + "ID": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'BuildHealth'}", // disambiguate from other Alpha_Omegasis mods "UpperVersion": "1.3", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/446", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Buy Cooking Recipes", - "ID": [ "Denifia.BuyRecipes" ], + "ID": "Denifia.BuyRecipes", "UpperVersion": "1.0", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1126", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Buy Back Collectables", - "ID": [ "BuyBackCollectables" ], + "ID": "BuyBackCollectables", "UpperVersion": "1.3", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/507", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Chest Label System", - "ID": [ "SPDChestLabel" ], + "ID": "SPDChestLabel", "UpperVersion": "1.6", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/242", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SDV 1.1." }, { "Name": "Chest Pooling", - "ID": [ "ChestPooling.dll" ], + "ID": "ChestPooling.dll", "UpperVersion": "1.2", "UpdateUrls": [ "http://community.playstarbound.com/threads/111988" ], "Notes": "Broke in SDV 1.2." }, { "Name": "Chests Anywhere", - "ID": [ "ChestsAnywhere", /*since 1.9*/ "Pathoschild.ChestsAnywhere" ], + "ID": "ChestsAnywhere | Pathoschild.ChestsAnywhere", // changed in 1.9 "UpperVersion": "1.9-beta", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/518" ], "Notes": "Broke in SDV 1.2." }, { "Name": "Choose Baby Gender", - "ID": [ "ChooseBabyGender.dll" ], + "ID": "ChooseBabyGender.dll", "UpperVersion": "1.0.2", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/590", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "CJB Automation", - "ID": [ "CJBAutomation" ], + "ID": "CJBAutomation", "UpperVersion": "1.4", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/211", "http://www.nexusmods.com/stardewvalley/mods/1063" ], "Notes": "Broke in SDV 1.2." }, { "Name": "CJB Cheats Menu", - "ID": [ "CJBCheatsMenu" ], + "ID": "CJBCheatsMenu", "UpperVersion": "1.12", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/4" ], "Notes": "Broke in SDV 1.1." }, { "Name": "CJB Item Spawner", - "ID": [ "CJBItemSpawner" ], + "ID": "CJBItemSpawner", "UpperVersion": "1.5", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/93" ], "Notes": "Broke in SDV 1.1." }, { "Name": "CJB Show Item Sell Price", - "ID": [ "CJBShowItemSellPrice" ], + "ID": "CJBShowItemSellPrice", "UpperVersion": "1.6", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/93" ], "Notes": "Broke in SDV 1.2." }, { "Name": "Climates of Ferngill", - "ID": [ "KoihimeNakamura.ClimatesOfFerngill" ], + "ID": "KoihimeNakamura.ClimatesOfFerngill", "UpperVersion": "1.0", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/604", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Cold Weather Haley", - "ID": [ "LordXamon.ColdWeatherHaleyPRO" ], + "ID": "LordXamon.ColdWeatherHaleyPRO", "UpperVersion": "1.0", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1169", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Colored Chests", - "ID": [ "4befde5c-731c-4853-8e4b-c5cdf946805f" ], + "ID": "4befde5c-731c-4853-8e4b-c5cdf946805f", "Status": "Obsolete", "ReasonPhrase": "colored chests were added in Stardew Valley 1.1." }, { "Name": "Combat with Farm Implements", - "ID": [ "SPDFarmingImplementsInCombat" ], + "ID": "SPDFarmingImplementsInCombat", "UpperVersion": "1.0", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/313", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Configurable Shipping Dates", - "ID": [ "ConfigurableShippingDates" ], + "ID": "ConfigurableShippingDates", "UpperVersion": "1.1.1", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/675", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Cooking Skill", - "ID": [ "CookingSkill", /*since 1.0.4–6*/ "spacechase0.CookingSkill" ], + "ID": "CookingSkill | spacechase0.CookingSkill", // changed in 1.0.4–6 "UpperVersion": "1.0.6", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/522" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "CrabNet", - "ID": [ "CrabNet.dll" ], + "ID": "CrabNet.dll", "UpperVersion": "1.0.4", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/584", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Customize Exterior", - "ID": [ "CustomizeExterior" ], + "ID": "CustomizeExterior", "UpperVersion": "1.0.2", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1099" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Customizable Traveling Cart Days", - "ID": [ "TravelingCartYyeahdude" ], + "ID": "TravelingCartYyeahdude", "UpperVersion": "1.0", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/567", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Daily News", - "ID": [ "bashNinja.DailyNews" ], + "ID": "bashNinja.DailyNews", "UpperVersion": "1.1", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1141", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Dynamic Checklist", - "ID": [ "gunnargolf.DynamicChecklist" ], + "ID": "gunnargolf.DynamicChecklist", "UpperVersion": "1.0", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1145", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Dynamic Machines", - "ID": [ "DynamicMachines" ], + "ID": "DynamicMachines", "UpperVersion": "1.1", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/374", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Empty Hands", - "ID": [ "QuicksilverFox.EmptyHands" ], + "ID": "QuicksilverFox.EmptyHands", "UpperVersion": "1.0", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1176", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Enemy Health Bars", - "ID": [ "SPDHealthBar" ], + "ID": "SPDHealthBar", "UpperVersion": "1.7", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/193", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SDV 1.2." }, { "Name": "Entoarox Framework", - "ID": [ "eacdb74b-4080-4452-b16b-93773cda5cf9", /*since ???*/ "Entoarox.EntoaroxFramework" ], + "ID": "eacdb74b-4080-4452-b16b-93773cda5cf9 | Entoarox.EntoaroxFramework", // changed in ??? "UpperVersion": "1.7.9", "UpdateUrls": [ "http://community.playstarbound.com/resources/4228" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Extended Fridge", - "ID": [ "Mystra007ExtendedFridge" ], + "ID": "Mystra007ExtendedFridge", "UpperVersion": "1.0", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/485", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SDV 1.2." }, { "Name": "Extended Greenhouse", - "ID": [ "ExtendedGreenhouse" ], + "ID": "ExtendedGreenhouse", "UpperVersion": "1.0.2", "UpdateUrls": [ "http://community.playstarbound.com/resources/4303", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SDV 1.2." }, { "Name": "Fall 28 Snow Day", - "ID": [ "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Fall28 Snow Day'}" ], // disambiguate from other mods by Alpha_Omegasis + "ID": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Fall28 Snow Day'}", // disambiguate from other mods by Alpha_Omegasis "UpperVersion": "1.3", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/486", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Farm Automation: Barn Door Automation", - "ID": [ "FarmAutomation.BarnDoorAutomation.dll" ], + "ID": "FarmAutomation.BarnDoorAutomation.dll", "UpperVersion": "1.0", "UpdateUrls": [ "http://community.playstarbound.com/threads/111931", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Farm Automation: Item Collector", - "ID": [ "FarmAutomation.ItemCollector.dll" ], + "ID": "FarmAutomation.ItemCollector.dll", "UpperVersion": "1.0", "UpdateUrls": [ "http://community.playstarbound.com/threads/111931", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SDV 1.2." }, { "Name": "Farm Automation Unofficial: Item Collector", - "ID": [ "Maddy99.FarmAutomation.ItemCollector" ], + "ID": "Maddy99.FarmAutomation.ItemCollector", "UpperVersion": "0.5", "UpdateUrls": [ "http://community.playstarbound.com/threads/125172", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Farm Expansion", - "ID": [ "3888bdfd-73f6-4776-8bb7-8ad45aea1915", /*since 2.0*/ "AdvizeFarmExpansionMod-2-0", /*since 2.0.5*/ "AdvizeFarmExpansionMod-2-0-5" ], + "ID": "3888bdfd-73f6-4776-8bb7-8ad45aea1915 | AdvizeFarmExpansionMod-2-0 | AdvizeFarmExpansionMod-2-0-5", // changed in 2.0 and 2.0.5 "UpperVersion": "2.0.5", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/130", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Farm Resource Generator", - "ID": [ "FarmResourceGenerator.dll" ], + "ID": "FarmResourceGenerator.dll", "UpperVersion": "1.0.4", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/647", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Faster Run", - "ID": [ "FasterRun.dll" ], + "ID": "FasterRun.dll", "UpperVersion": "1.1", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/733", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "FlorenceMod", - "ID": [ "FlorenceMod.dll" ], + "ID": "FlorenceMod.dll", "UpperVersion": "1.1", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/591", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Forage at the Farm", - "ID": [ "ForageAtTheFarm" ], + "ID": "ForageAtTheFarm", "UpperVersion": "1.5.1", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/673", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Instant Geode", - "ID": [ "InstantGeode" ], + "ID": "InstantGeode", "UpperVersion": "1.12", "UpdateUrls": [ "http://community.playstarbound.com/threads/109038", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SDV 1.2." }, { "Name": "Gate Opener", - "ID": [ "GateOpener.dll", /*since 1.1*/ "mralbobo.GateOpener" ], + "ID": "GateOpener.dll | mralbobo.GateOpener", // changed in 1.1 "UpperVersion": "1.0.1", "UpdateUrls": [ "http://community.playstarbound.com/threads/111988" ], "Notes": "Broke in SDV 1.2." }, { "Name": "GenericShopExtender", - "ID": [ "GenericShopExtender" ], + "ID": "GenericShopExtender", "UpperVersion": "0.1.2", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/814", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Get Dressed", - "ID": [ "GetDressed.dll", /*since 3.3*/ "Advize.GetDressed" ], + "ID": "GetDressed.dll | Advize.GetDressed", // changed in 3.3 "UpperVersion": "3.3", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/331" ], "Notes": "Broke in SDV 1.2." }, { "Name": "Gift Taste Helper", - "ID": [ "8008db57-fa67-4730-978e-34b37ef191d6" ], + "ID": "8008db57-fa67-4730-978e-34b37ef191d6", "UpperVersion": "2.3.1", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/229" ], "Notes": "Broke in SDV 1.2." }, { "Name": "Happy Animals", - "ID": [ "HappyAnimals" ], + "ID": "HappyAnimals", "UpperVersion": "1.0.3", "UpdateUrls": [ "https://community.playstarbound.com/threads/126655", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Happy Birthday", - "ID": [ "{ID:'HappyBirthday', Author:'Alpha_Omegasis'}" ], // disambiguate from Oxyligen's fork + "ID": "{ID:'HappyBirthday', Author:'Alpha_Omegasis'}", // disambiguate from Oxyligen's fork "UpperVersion": "1.3", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/520", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Harvest With Scythe", - "ID": [ "965169fd-e1ed-47d0-9f12-b104535fb4bc" ], + "ID": "965169fd-e1ed-47d0-9f12-b104535fb4bc", "UpperVersion": "1.0.6", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/236", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Hunger for Food", - "ID": [ "HungerForFoodByTigerle" ], + "ID": "HungerForFoodByTigerle", "UpperVersion": "0.1.2", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/810", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Improved Quality of Life", - "ID": [ "Demiacle.ImprovedQualityOfLife" ], + "ID": "Demiacle.ImprovedQualityOfLife", "UpperVersion": "1.1", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1025", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Less Strict Over-Exertion (AntiExhaustion)", - "ID": [ "BALANCEMOD_AntiExhaustion" ], + "ID": "BALANCEMOD_AntiExhaustion", "UpperVersion": "1.1", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/637", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Lookup Anything", - "ID": [ "LookupAnything", /*since 1.10.1*/ "Pathoschild.LookupAnything" ], + "ID": "LookupAnything | Pathoschild.LookupAnything", // changed in 1.10.1 "UpperVersion": "1.10.1", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/541" ], "Notes": "Broke in SDV 1.2." }, { "Name": "Loved Labels", - "ID": [ "LovedLabels.dll" ], + "ID": "LovedLabels.dll", "UpperVersion": "2.0", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/279" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Luck Skill", - "ID": [ "LuckSkill", /*since 0.1.4*/ "spacechase0.LuckSkill" ], + "ID": "LuckSkill | spacechase0.LuckSkill", // changed in 0.1.4 "UpperVersion": "0.1.4", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/521" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "MailOrderPigs", - "ID": [ "MailOrderPigs.dll" ], + "ID": "MailOrderPigs.dll", "UpperVersion": "1.0.1", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/632", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Makeshift Multiplayer", - "ID": [ "StardewValleyMP", /*since 0.3*/ "spacechase0.StardewValleyMP" ], + "ID": "StardewValleyMP | spacechase0.StardewValleyMP", // changed in 0.3 "UpperVersion": "0.3.6", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/501", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Message Box [API]? (ChatMod)", - "ID": [ "Kithio:ChatMod" ], + "ID": "Kithio:ChatMod", "UpperVersion": "1.0", "UpdateUrls": [ "http://community.playstarbound.com/resources/message-box-api.4296", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Modder Serialization Utility", - "ID": [ "SerializerUtils-0-1" ], + "ID": "SerializerUtils-0-1", "Status": "Obsolete", "ReasonPhrase": "it's no longer maintained or used." }, { "Name": "More Artifact Spots", - "ID": [ "451" ], + "ID": "451", "UpperVersion": "1.0.1", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/451", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "More Pets", - "ID": [ "821ce8f6-e629-41ad-9fde-03b54f68b0b6MOREPETS", /* since 1.3 */ "Entoarox.MorePets" ], + "ID": "821ce8f6-e629-41ad-9fde-03b54f68b0b6MOREPETS | Entoarox.MorePets", // changed in 1.3 "UpperVersion": "1.3.2", "UpdateUrls": [ "http://community.playstarbound.com/resources/4288" ], "Notes": "Overhauled for SMAPI 1.11+ compatibility." }, { "Name": "More Rain", - "ID": [ "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'More_Rain'}" ], // disambiguate from other mods by Alpha_Omegasis + "ID": "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'More_Rain'}", // disambiguate from other mods by Alpha_Omegasis "UpperVersion": "1.4", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/441", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Multiple Sprites and Portraits On Rotation (File Loading)", - "ID": [ "FileLoading" ], + "ID": "FileLoading", "UpperVersion": "1.12", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1094", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Museum Rearranger", - "ID": [ "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Museum Rearranger'}" ], // disambiguate from other mods by Alpha_Omegasis + "ID": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Museum Rearranger'}", // disambiguate from other mods by Alpha_Omegasis "UpperVersion": "1.3", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/428", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "New Machines", - "ID": [ "F70D4FAB-0AB2-4B78-9F1B-AF2CA2236A59" ], + "ID": "F70D4FAB-0AB2-4B78-9F1B-AF2CA2236A59", "UpperVersion": "4.2.1343", "UpdateUrls": [ "http://community.playstarbound.com/resources/3683", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Night Owl", - "ID": [ "{'ID':'SaveAnywhere', 'Name':'Stardew_NightOwl'}" ], // disambiguate from Save Anywhere + "ID": "{ID:'SaveAnywhere', Name:'Stardew_NightOwl'}", // disambiguate from Save Anywhere "UpperVersion": "2.1", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/433", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "No Debug Mode", - "ID": [ "NoDebugMode" ], + "ID": "NoDebugMode", "Status": "Obsolete", "ReasonPhrase": "debug mode was removed in SMAPI 1.0." }, { "Name": "NoSoilDecay", - "ID": [ "289dee03-5f38-4d8e-8ffc-e440198e8610" ], + "ID": "289dee03-5f38-4d8e-8ffc-e440198e8610", "UpperVersion": "0.5", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/237", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SDV 1.2, and uses Assembly.GetExecutingAssembly().Location." }, { "Name": "NPC Map Locations", - "ID": [ "NPCMapLocationsMod" ], + "ID": "NPCMapLocationsMod", "LowerVersion": "1.42", "UpperVersion": "1.43", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/239" ], @@ -577,293 +577,293 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, { "Name": "NPC Speak", - "ID": [ "NpcEcho.dll" ], + "ID": "NpcEcho.dll", "UpperVersion": "1.0", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/694", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0" }, { "Name": "OmniFarm", - "ID": [ "BlueMod_OmniFarm" ], + "ID": "BlueMod_OmniFarm", "UpperVersion": "2.0.1", "UpdateUrls": [ "http://community.playstarbound.com/threads/127299", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0" }, { "Name": "Part of the Community", - "ID": [ "SB_PotC" ], + "ID": "SB_PotC", "UpperVersion": "1.0.8", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/923" ], "Notes": "Broke in SDV 1.2." }, { "Name": "PelicanFiber", - "ID": [ "PelicanFiber.dll" ], + "ID": "PelicanFiber.dll", "UpperVersion": "3.0", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/631", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "PelicanTTS", - "ID": [ "Platonymous.PelicanTTS" ], + "ID": "Platonymous.PelicanTTS", "UpperVersion": "1.6", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1079", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Persival's BundleMod", - "ID": [ "BundleMod.dll" ], + "ID": "BundleMod.dll", "UpperVersion": "1.0", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/438", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SDV 1.1." }, { "Name": "Point-and-Plant", - "ID": [ "PointAndPlant.dll" ], + "ID": "PointAndPlant.dll", "UpperVersion": "1.0.2", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/572", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SDV 1.2." }, { "Name": "PrairieKingMadeEasy", - "ID": [ "PrairieKingMadeEasy.dll" ], + "ID": "PrairieKingMadeEasy.dll", "UpperVersion": "1.0", "UpdateUrls": [ "http://community.playstarbound.com/resources/3594", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SDV 1.2." }, { "Name": "RainRandomizer", - "ID": [ "RainRandomizer.dll" ], + "ID": "RainRandomizer.dll", "UpperVersion": "1.0.3", "UpdateUrls": [ "http://community.playstarbound.com/threads/111526", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "RelationshipsEnhanced", - "ID": [ "relationshipsenhanced" ], + "ID": "relationshipsenhanced", "UpperVersion": "1.0", "UpdateUrls": [ "http://community.playstarbound.com/resources/4435", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "RelationShipStatus", - "ID": [ "relationshipstatus" ], + "ID": "relationshipstatus", "UpperVersion": "1.0.5", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/751", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Replanter", - "ID": [ "Replanter.dll" ], + "ID": "Replanter.dll", "UpperVersion": "1.0.4", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/589", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Reusable Wallpapers and Floors (Wallpaper Retain)", - "ID": [ "dae1b553-2e39-43e7-8400-c7c5c836134b" ], + "ID": "dae1b553-2e39-43e7-8400-c7c5c836134b", "UpperVersion": "1.5", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/356", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Rush Orders", - "ID": [ "RushOrders", /*since 1.1*/ "spacechase0.RushOrders" ], + "ID": "RushOrders | spacechase0.RushOrders", // changed in 1.1 "UpperVersion": "1.1", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/605" ], "Notes": "Broke in SDV 1.2." }, { "Name": "Save Anywhere", - "ID": [ "{'ID':'SaveAnywhere', 'Name':'Save Anywhere'}" ], // disambiguate from Night Owl + "ID": "{ID:'SaveAnywhere', Name:'Save Anywhere'}", // disambiguate from Night Owl "UpperVersion": "2.4", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/444", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Seasonal Immersion", - "ID": [ "EntoaroxSeasonalHouse", /*since 1.1.0*/ "EntoaroxSeasonalBuildings", /* since 1.6 or earlier*/ "EntoaroxSeasonalImmersion", /*since 1.7*/ "Entoarox.SeasonalImmersion" ], + "ID": "EntoaroxSeasonalHouse | EntoaroxSeasonalBuildings | EntoaroxSeasonalImmersion | Entoarox.SeasonalImmersion", // changed in 1.1, 1.6 or earlier, and 1.7 "UpperVersion": "1.8.2", "UpdateUrls": [ "http://community.playstarbound.com/resources/4262", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Send Items", - "ID": [ "Denifia.SendItems" ], + "ID": "Denifia.SendItems", "UpperVersion": "1.0.2", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1087", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Shed Notifications (BuildingsNotifications)", - "ID": [ "TheCroak.BuildingsNotifications" ], + "ID": "TheCroak.BuildingsNotifications", "UpperVersion": "0.4.1", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/620", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Shenandoah Project", - "ID": [ "Shenandoah Project" ], + "ID": "Shenandoah Project", "UpperVersion": "1.1.1", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/756", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Shipment Tracker", - "ID": [ "7e474181-e1a0-40f9-9c11-d08a3dcefaf3" ], + "ID": "7e474181-e1a0-40f9-9c11-d08a3dcefaf3", "UpperVersion": "1.1", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/321", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Shop Expander", - "ID": [ /*since at least 1.4*/ "821ce8f6-e629-41ad-9fde-03b54f68b0b6", /*since 1.5*/ "EntoaroxShopExpander", /*since 1.5.2*/ "Entoarox.ShopExpander" ], + "ID": "821ce8f6-e629-41ad-9fde-03b54f68b0b6 | EntoaroxShopExpander | Entoarox.ShopExpander", // changed in 1.5 and 1.5.2 "UpperVersion": "1.5.3", "UpdateUrls": [ "http://community.playstarbound.com/resources/4381", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Showcase Mod", - "ID": [ "Igorious.Showcase" ], + "ID": "Igorious.Showcase", "UpperVersion": "0.9", "UpdateUrls": [ "http://community.playstarbound.com/resources/4487", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Simple Sprinklers", - "ID": [ "SimpleSprinkler.dll" ], + "ID": "SimpleSprinkler.dll", "UpperVersion": "1.4", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/76", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SDV 1.2." }, { "Name": "Siv's Marriage Mod", - "ID": [ "6266959802" ], + "ID": "6266959802", "UpperVersion": "1.2.2", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/366", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 1.9 (has multiple Mod instances)." }, { "Name": "Skill Prestige", - "ID": [ "6b843e60-c8fc-4a25-a67b-4a38ac8dcf9b" ], + "ID": "6b843e60-c8fc-4a25-a67b-4a38ac8dcf9b", "UpperVersion": "1.0.9", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/569", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Skill Prestige: Cooking Adapter", - "ID": [ "20d6b8a3-b6e7-460b-a6e4-07c2b0cb6c63" ], + "ID": "20d6b8a3-b6e7-460b-a6e4-07c2b0cb6c63", "UpperVersion": "1.0.9", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/569", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Slower Fence Decay", - "ID": [ "SPDSlowFenceDecay" ], + "ID": "SPDSlowFenceDecay", "UpperVersion": "0.5.1", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/252", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Smart Mod", - "ID": [ "KuroBear.SmartMod" ], + "ID": "KuroBear.SmartMod", "UpperVersion": "2.2", "UpdateUrls": [ "http://community.playstarbound.com/threads/108104", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Solar Eclipse Event", - "ID": [ "KoihimeNakamura.SolarEclipseEvent" ], + "ID": "KoihimeNakamura.SolarEclipseEvent", "UpperVersion": "1.1", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/897", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Sprinkles", - "ID": [ "Platonymous.Sprinkles" ], + "ID": "Platonymous.Sprinkles", "UpperVersion": "1.1.3", "UpdateUrls": [ "http://community.playstarbound.com/resources/4592", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Sprint and Dash", - "ID": [ "SPDSprintAndDash" ], + "ID": "SPDSprintAndDash", "UpperVersion": "1.0", "UpdateUrls": [ "http://community.playstarbound.com/resources/3531", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SDV 1.2." }, { "Name": "Sprint and Dash Redux", - "ID": [ "SPDSprintAndDash" ], + "ID": "SPDSprintAndDash", "UpperVersion": "1.2", "UpdateUrls": [ "http://community.playstarbound.com/resources/4201" ], "Notes": "Broke in SDV 1.2." }, { "Name": "Sprinting Mod", - "ID": [ "a10d3097-b073-4185-98ba-76b586cba00c" ], + "ID": "a10d3097-b073-4185-98ba-76b586cba00c", "UpperVersion": "2.1", "UpdateUrls": [ "http://community.playstarbound.com/threads/108313", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SDV 1.2." }, { "Name": "StackSplitX", - "ID": [ "StackSplitX.dll" ], + "ID": "StackSplitX.dll", "UpperVersion": "1.2", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/798" ], "Notes": "Broke in SDV 1.2." }, { "Name": "StaminaRegen", - "ID": [ "StaminaRegen.dll" ], + "ID": "StaminaRegen.dll", "UpperVersion": "1.0.3", "UpdateUrls": [ "http://community.playstarbound.com/threads/111526", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Stardew Auto Backup", - "ID": [ "{'ID':'4be88c18-b6f3-49b0-ba96-f94b1a5be890', 'Name':'Stardew_Save_Backup'}" ], // disambiguate from other Alpha_Omegasis mods + "ID": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'Stardew_Save_Backup'}", // disambiguate from other Alpha_Omegasis mods "UpperVersion": "1.2", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/435", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Stardew Notification", - "ID": [ "stardewnotification" ], + "ID": "stardewnotification", "UpperVersion": "1.7", "UpdateUrls": [ "http://community.playstarbound.com/threads/127979", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Stardew Symphony", - "ID": [ "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'Stardew_Symphony'}" ], // disambiguate other mods by Alpha_Omegasis + "ID": "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'Stardew_Symphony'}", // disambiguate other mods by Alpha_Omegasis "UpperVersion": "1.3", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/425", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "StarDustCore", - "ID": [ "StarDustCore" ], + "ID": "StarDustCore", "Status": "Obsolete", "ReasonPhrase": "it was only used by earlier versions of Save Anywhere, and is no longer used or maintained." }, { "Name": "StashItemsToChest", - "ID": [ "BlueMod_StashItemsToChest" ], + "ID": "BlueMod_StashItemsToChest", "UpperVersion": "1.0.1", "UpdateUrls": [ "http://community.playstarbound.com/threads/126906", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Stone Bridge Over Pond (PondWithBridge)", - "ID": [ "PondWithBridge.dll" ], + "ID": "PondWithBridge.dll", "UpperVersion": "1.0", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/316", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Super Greenhouse Warp Modifier", - "ID": [ "SuperGreenhouse" ], + "ID": "SuperGreenhouse", "UpperVersion": "1.0", "UpperVersionLabel": "1.0 (2016-11-29)", "UpdateUrls": [ "http://community.playstarbound.com/resources/4334", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], @@ -871,132 +871,132 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, { "Name": "Tainted Cellar", - "ID": [ "TaintedCellar.dll" ], + "ID": "TaintedCellar.dll", "UpperVersion": "1.0", "UpdateUrls": [ "http://community.playstarbound.com/threads/115735", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SDV 1.1 or 1.11." }, { "Name": "Teleporter", - "ID": [ "Teleporter" ], + "ID": "Teleporter", "UpperVersion": "1.0.2", "UpdateUrls": [ "http://community.playstarbound.com/resources/4374", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SDV 1.2." }, { "Name": "Three-heart Dance Partner", - "ID": [ "ThreeHeartDancePartner" ], + "ID": "ThreeHeartDancePartner", "UpperVersion": "1.0.1", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/500", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SDV 1.2." }, { "Name": "TimeSpeed", - "ID": [ "TimeSpeed.dll", /* since 2.0.3 */ "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'TimeSpeed'}", /* since 2.0.3 */ "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'TimeSpeed Mod (unofficial)'}", /*since 2.1*/ "community.TimeSpeed" ], // disambiguate other mods by Alpha_Omegasis + "ID": "TimeSpeed.dll | {ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'TimeSpeed'} | {ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'TimeSpeed Mod (unofficial)'} | community.TimeSpeed", // changed in 2.0.3 and 2.1; disambiguate other mods by Alpha_Omegasis "UpperVersion": "2.2", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/169" ], "Notes": "Broke in SDV 1.2." }, { "Name": "UiModSuite", - "ID": [ "Demiacle.UiModSuite" ], + "ID": "Demiacle.UiModSuite", "UpperVersion": "1.0", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1023", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SDV 1.2." }, { "Name": "WakeUp", - "ID": [ "WakeUp.dll" ], + "ID": "WakeUp.dll", "UpperVersion": "1.0.2", "UpdateUrls": [ "http://community.playstarbound.com/threads/111526", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Wallpaper Fix", - "ID": [ "WallpaperFix.dll" ], + "ID": "WallpaperFix.dll", "UpperVersion": "1.1", "UpdateUrls": [ "http://community.playstarbound.com/resources/4211", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "Weather Controller", - "ID": [ "WeatherController.dll" ], + "ID": "WeatherController.dll", "UpperVersion": "1.0.2", "UpdateUrls": [ "http://community.playstarbound.com/threads/111526", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SDV 1.2." }, { "Name": "Wonderful Farm Life", - "ID": [ "WonderfulFarmLife.dll" ], + "ID": "WonderfulFarmLife.dll", "UpperVersion": "1.0", "UpdateUrls": [ "http://community.playstarbound.com/threads/115384", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SDV 1.1 or 1.11." }, { "Name": "XmlSerializerRetool", - "ID": [ "XmlSerializerRetool.dll" ], + "ID": "XmlSerializerRetool.dll", "Status": "Obsolete", "ReasonPhrase": "it's no longer maintained or used." }, { "Name": "Xnb Loader", - "ID": [ "Entoarox.XnbLoader" ], + "ID": "Entoarox.XnbLoader", "UpperVersion": "1.0.6", "UpdateUrls": [ "http://community.playstarbound.com/resources/4506", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Notes": "Broke in SMAPI 2.0." }, { "Name": "zDailyIncrease", - "ID": [ "zdailyincrease" ], + "ID": "zdailyincrease", "UpperVersion": "1.2", "UpdateUrls": [ "http://community.playstarbound.com/resources/4247" ], "Notes": "Broke in SDV 1.2." }, { "Name": "Zoom Out Extreme", - "ID": [ "ZoomMod" ], + "ID": "ZoomMod", "UpperVersion": "0.1", "UpdateUrls": [ "http://community.playstarbound.com/threads/115028" ], "Notes": "Broke in SDV 1.2." }, { "Name": "Zoryn's Better RNG", - "ID": [ "76b6d1e1-f7ba-4d72-8c32-5a1e6d2716f6", /*since 1.6*/ "Zoryn.BetterRNG" ], + "ID": "76b6d1e1-f7ba-4d72-8c32-5a1e6d2716f6 | Zoryn.BetterRNG", // changed in 1.6 "UpperVersion": "1.6", "UpdateUrls": [ "https://github.com/Zoryn4163/SMAPI-Mods/releases" ], "Notes": "Broke in SDV 1.2." }, { "Name": "Zoryn's Calendar Anywhere", - "ID": [ "a41c01cd-0437-43eb-944f-78cb5a53002a", /*since 1.6*/ "Zoryn.CalendarAnywhere" ], + "ID": "a41c01cd-0437-43eb-944f-78cb5a53002a | Zoryn.CalendarAnywhere", // changed in 1.6 "UpperVersion": "1.6", "UpdateUrls": [ "https://github.com/Zoryn4163/SMAPI-Mods/releases" ], "Notes": "Broke in SDV 1.2." }, { "Name": "Zoryn's Health Bars", - "ID": [ "HealthBars.dll", /*since 1.6*/ "Zoryn.HealthBars" ], + "ID": "HealthBars.dll | Zoryn.HealthBars", // changed in 1.6 "UpperVersion": "1.6", "UpdateUrls": [ "https://github.com/Zoryn4163/SMAPI-Mods/releases" ], "Notes": "Broke in SDV 1.2." }, { "Name": "Zoryn's Junimo Deposit Anywhere", - "ID": [ "f93a4fe8-cade-4146-9335-b5f82fbbf7bc", /*since 1.6*/ "Zoryn.JunimoDepositAnywhere" ], + "ID": "f93a4fe8-cade-4146-9335-b5f82fbbf7bc | Zoryn.JunimoDepositAnywhere", // changed in 1.6 "UpperVersion": "1.7", "UpdateUrls": [ "https://github.com/Zoryn4163/SMAPI-Mods/releases" ], "Notes": "Broke in SDV 1.2." }, { "Name": "Zoryn's Movement Mod", - "ID": [ "8a632929-8335-484f-87dd-c29d2ba3215d", /*since 1.6*/ "Zoryn.MovementModifier" ], + "ID": "8a632929-8335-484f-87dd-c29d2ba3215d | Zoryn.MovementModifier", // changed in 1.6 "UpperVersion": "1.6", "UpdateUrls": [ "https://github.com/Zoryn4163/SMAPI-Mods/releases" ], "Notes": "Broke in SDV 1.2." }, { "Name": "Zoryn's Regen Mod", - "ID": [ "dfac4383-1b6b-4f33-ae4e-37fc23e5252e", /*since 1.6*/ "Zoryn.RegenMod" ], + "ID": "dfac4383-1b6b-4f33-ae4e-37fc23e5252e | Zoryn.RegenMod", // changed in 1.6 "UpperVersion": "1.6", "UpdateUrls": [ "https://github.com/Zoryn4163/SMAPI-Mods/releases" ], "Notes": "Broke in SDV 1.2." From 0863f9b7e5f165f2b1db8750b20ed35bc0c3701a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 Sep 2017 00:23:48 -0400 Subject: [PATCH 099/186] revamp mod compatibility fields to allow broader use of mod data records (#361) --- .../Core/ModResolverTests.cs | 6 +- .../Framework/ModLoading/ModResolver.cs | 45 +- .../Framework/Models/ModCompatibility.cs | 55 ++ .../Framework/Models/ModDataRecord.cs | 30 +- .../Serialisation/SFieldConverter.cs | 22 +- src/StardewModdingAPI/Program.cs | 2 +- .../StardewModdingAPI.config.json | 688 +++++++++++------- .../StardewModdingAPI.csproj | 1 + 8 files changed, 524 insertions(+), 325 deletions(-) create mode 100644 src/StardewModdingAPI/Framework/Models/ModCompatibility.cs diff --git a/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs b/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs index 4dbd1438..2d65eee9 100644 --- a/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs +++ b/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs @@ -141,7 +141,11 @@ namespace StardewModdingAPI.Tests.Core { // arrange Mock mock = this.GetMetadata("Mod A", new string[0], allowStatusChange: true); - this.SetupMetadataForValidation(mock, new ModDataRecord { Status = ModStatus.AssumeBroken, UpperVersion = new SemanticVersion("1.0"), UpdateUrls = new[] { "http://example.org" } }); + this.SetupMetadataForValidation(mock, new ModDataRecord + { + Compatibility = new[] { new ModCompatibility("~1.0", ModStatus.AssumeBroken, null) }, + UpdateUrls = new[] { "http://example.org" } + }); // act new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0")); diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs index 3674faec..31ba5bc5 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs @@ -61,14 +61,7 @@ namespace StardewModdingAPI.Framework.ModLoading string key = !string.IsNullOrWhiteSpace(manifest.UniqueID) ? manifest.UniqueID : manifest.EntryDll; // get data record - dataRecord = ( - from mod in dataRecords - where - mod.ID.Matches(key, manifest) - && (mod.LowerVersion == null || !manifest.Version.IsOlderThan(mod.LowerVersion)) - && (mod.UpperVersion == null || !manifest.Version.IsNewerThan(mod.UpperVersion)) - select mod - ).FirstOrDefault(); + dataRecord = dataRecords.FirstOrDefault(record => record.ID.Matches(key, manifest)); } // build metadata @@ -98,28 +91,26 @@ namespace StardewModdingAPI.Framework.ModLoading continue; // validate compatibility + ModCompatibility compatibility = mod.DataRecord?.GetCompatibility(mod.Manifest.Version); + switch (compatibility?.Status) { - ModDataRecord dataRecord = mod.DataRecord; - switch (dataRecord?.Status) - { - case ModStatus.Obsolete: - mod.SetStatus(ModMetadataStatus.Failed, $"it's obsolete: {dataRecord.ReasonPhrase}"); - continue; + case ModStatus.Obsolete: + mod.SetStatus(ModMetadataStatus.Failed, $"it's obsolete: {compatibility.ReasonPhrase}"); + continue; - case ModStatus.AssumeBroken: - { - string reasonPhrase = dataRecord.ReasonPhrase ?? "it's no longer compatible"; - string error = $"{reasonPhrase}. Please check for a "; - if (mod.Manifest.Version.Equals(dataRecord.UpperVersion) && dataRecord.UpperVersionLabel == null) - error += "newer version"; - else - error += $"version newer than {dataRecord.UpperVersionLabel ?? dataRecord.UpperVersion.ToString()}"; - error += " at " + string.Join(" or ", dataRecord.UpdateUrls); + case ModStatus.AssumeBroken: + { + string reasonPhrase = compatibility.ReasonPhrase ?? "it's no longer compatible"; + string error = $"{reasonPhrase}. Please check for a "; + if (mod.Manifest.Version.Equals(compatibility.UpperVersion)) + error += "newer version"; + else + error += $"version newer than {compatibility.UpperVersion}"; + error += " at " + string.Join(" or ", mod.DataRecord.UpdateUrls); - mod.SetStatus(ModMetadataStatus.Failed, error); - continue; - } - } + mod.SetStatus(ModMetadataStatus.Failed, error); + } + continue; } // validate SMAPI version diff --git a/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs b/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs new file mode 100644 index 00000000..54737e6c --- /dev/null +++ b/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs @@ -0,0 +1,55 @@ +using System; + +namespace StardewModdingAPI.Framework.Models +{ + /// Specifies the compatibility of a given mod version range. + internal class ModCompatibility + { + /********* + ** Accessors + *********/ + /// The lowest version in the range, or null for all past versions. + public ISemanticVersion LowerVersion { get; } + + /// The highest version in the range, or null for all future versions. + public ISemanticVersion UpperVersion { get; } + + /// The mod compatibility. + public ModStatus Status { get; } + + /// The reason phrase to show in log output, or null to use the default value. + /// For example, "this version is incompatible with the latest version of the game". + public string ReasonPhrase { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// A version range, which consists of two version strings separated by a '~' character. Either side can be left blank for an unbounded range. + /// The mod compatibility. + /// The reason phrase to show in log output, or null to use the default value. + public ModCompatibility(string versionRange, ModStatus status, string reasonPhrase) + { + // extract version strings + string[] versions = versionRange.Split('~'); + if (versions.Length != 2) + throw new FormatException($"Could not parse '{versionRange}' as a version range. It must have two version strings separated by a '~' character (either side can be left blank for an unbounded range)."); + + // initialise + this.LowerVersion = !string.IsNullOrWhiteSpace(versions[0]) ? new SemanticVersion(versions[0]) : null; + this.UpperVersion = !string.IsNullOrWhiteSpace(versions[1]) ? new SemanticVersion(versions[1]) : null; + this.Status = status; + this.ReasonPhrase = reasonPhrase; + } + + /// Get whether a given version is contained within this compatibility range. + /// The version to check. + public bool MatchesVersion(ISemanticVersion version) + { + return + (this.LowerVersion == null || !version.IsOlderThan(this.LowerVersion)) + && (this.UpperVersion == null || !version.IsNewerThan(this.UpperVersion)); + } + } +} diff --git a/src/StardewModdingAPI/Framework/Models/ModDataRecord.cs b/src/StardewModdingAPI/Framework/Models/ModDataRecord.cs index 8126022d..de219076 100644 --- a/src/StardewModdingAPI/Framework/Models/ModDataRecord.cs +++ b/src/StardewModdingAPI/Framework/Models/ModDataRecord.cs @@ -1,3 +1,4 @@ +using System.Linq; using Newtonsoft.Json; using StardewModdingAPI.Framework.Serialisation; @@ -16,25 +17,22 @@ namespace StardewModdingAPI.Framework.Models /// The mod name. public string Name { get; set; } - /// The oldest incompatible mod version, or null for all past versions. - [JsonConverter(typeof(SFieldConverter))] - public ISemanticVersion LowerVersion { get; set; } - - /// The most recent incompatible mod version. - [JsonConverter(typeof(SFieldConverter))] - public ISemanticVersion UpperVersion { get; set; } - - /// A label to show to the user instead of , when the manifest version differs from the user-facing version. - public string UpperVersionLabel { get; set; } - /// The URLs the user can check for a newer version. public string[] UpdateUrls { get; set; } - /// The reason phrase to show in the warning, or null to use the default value. - /// "this version is incompatible with the latest version of the game" - public string ReasonPhrase { get; set; } + /// The compatibility of given mod versions (if any). + [JsonConverter(typeof(SFieldConverter))] + public ModCompatibility[] Compatibility { get; set; } = new ModCompatibility[0]; - /// Indicates how SMAPI should treat the mod. - public ModStatus Status { get; set; } = ModStatus.AssumeBroken; + + /********* + ** Public methods + *********/ + /// Get the compatibility record for a given version, if any. + /// The mod version to check. + public ModCompatibility GetCompatibility(ISemanticVersion version) + { + return this.Compatibility.FirstOrDefault(p => p.MatchesVersion(version)); + } } } diff --git a/src/StardewModdingAPI/Framework/Serialisation/SFieldConverter.cs b/src/StardewModdingAPI/Framework/Serialisation/SFieldConverter.cs index d71e138b..ffece081 100644 --- a/src/StardewModdingAPI/Framework/Serialisation/SFieldConverter.cs +++ b/src/StardewModdingAPI/Framework/Serialisation/SFieldConverter.cs @@ -27,7 +27,8 @@ namespace StardewModdingAPI.Framework.Serialisation return objectType == typeof(ISemanticVersion) || objectType == typeof(IManifestDependency[]) - || objectType == typeof(ModDataID); + || objectType == typeof(ModDataID) + || objectType == typeof(ModCompatibility[]); } /// Reads the JSON representation of the object. @@ -68,7 +69,7 @@ namespace StardewModdingAPI.Framework.Serialisation } } - // manifest dependency + // manifest dependencies if (objectType == typeof(IManifestDependency[])) { List result = new List(); @@ -82,13 +83,28 @@ namespace StardewModdingAPI.Framework.Serialisation return result.ToArray(); } - // mod compatibility ID + // mod data ID if (objectType == typeof(ModDataID)) { JToken token = JToken.Load(reader); return new ModDataID(token.Value()); } + // mod compatibility records + if (objectType == typeof(ModCompatibility[])) + { + List result = new List(); + foreach (JProperty property in JObject.Load(reader).Properties()) + { + string range = property.Name; + ModStatus status = property.Value.Value(nameof(ModCompatibility.Status)); + string reasonPhrase = property.Value.Value(nameof(ModCompatibility.ReasonPhrase)); + + result.Add(new ModCompatibility(range, status, reasonPhrase)); + } + return result.ToArray(); + } + // unknown throw new NotSupportedException($"Unknown type '{objectType?.FullName}'."); } diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 63176dd1..4860968c 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -640,7 +640,7 @@ namespace StardewModdingAPI Assembly modAssembly; try { - modAssembly = modAssemblyLoader.Load(metadata, assemblyPath, assumeCompatible: metadata.DataRecord?.Status == ModStatus.AssumeCompatible); + modAssembly = modAssemblyLoader.Load(metadata, assemblyPath, assumeCompatible: metadata.DataRecord.GetCompatibility(metadata.Manifest.Version)?.Status == ModStatus.AssumeCompatible); } catch (IncompatibleInstructionException ex) { diff --git a/src/StardewModdingAPI/StardewModdingAPI.config.json b/src/StardewModdingAPI/StardewModdingAPI.config.json index c93ba130..3844b8b0 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.config.json +++ b/src/StardewModdingAPI/StardewModdingAPI.config.json @@ -48,958 +48,1092 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "AccessChestAnywhere", "ID": "AccessChestAnywhere", - "UpperVersion": "1.1", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/257", "http://www.nexusmods.com/stardewvalley/mods/518" ], - "Notes": "Broke in SDV 1.1." + "Compatibility": { + "~1.1": { "Status": "AssumeBroken" } // broke in SDV 1.1 + } }, { "Name": "AdjustArtisanPrices", "ID": "1e36d4ca-c7ef-4dfb-9927-d27a6c3c8bdc", - "UpperVersion": "0.0", - "UpperVersionLabel": "0.01", "UpdateUrls": [ "http://community.playstarbound.com/resources/3532", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 1.9." + "Compatibility": { + "~0.01": { "Status": "AssumeBroken" } // broke in SMAPI 1.9 + } }, { "Name": "Advanced Location Loader", "ID": "Entoarox.AdvancedLocationLoader", - "UpperVersion": "1.2.10", "UpdateUrls": [ "http://community.playstarbound.com/resources/3619" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.2.10": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "AgingMod", "ID": "skn.AgingMod", - "UpperVersion": "1.0", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1129", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Almighty Tool", "ID": "AlmightyTool.dll", - "UpperVersion": "1.1.1", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/439" ], - "Notes": "Broke in SDV 1.2." + "Compatibility": { + "~1.1.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } }, { "Name": "Animal Mood Fix", "ID": "GPeters-AnimalMoodFix", - "Status": "Obsolete", - "ReasonPhrase": "the animal mood bugs were fixed in Stardew Valley 1.2." + "Compatibility": { + "~": { "Status": "Obsolete", "ReasonPhrase": "the animal mood bugs were fixed in Stardew Valley 1.2." } + } }, { "Name": "Animal Sitter", "ID": "AnimalSitter.dll", - "UpperVersion": "1.0.8", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/581", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.0.8": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "A Tapper's Dream", "ID": "ddde5195-8f85-4061-90cc-0d4fd5459358", - "UpperVersion": "1.4", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/260", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Better Sprinklers", "ID": "SPDSprinklersMod | Speeder.BetterSprinklers", // changed in 2.3 - "UpperVersion": "2.3.1-pathoschild-update", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/41", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SDV 1.2." + "Compatibility": { + "~2.3.1-pathoschild-update": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } }, { "Name": "Birthday Mail", "ID": "005e02dc-d900-425c-9c68-1ff55c5a295d", - "UpperVersion": "1.2.2", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/276", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SDV 1.2." + "Compatibility": { + "~1.2.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } }, { "Name": "Build Endurance", "ID": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'BuildEndurance'}", // disambiguate from other Alpha_Omegasis mods - "UpperVersion": "1.3", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/445", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Build Health", "ID": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'BuildHealth'}", // disambiguate from other Alpha_Omegasis mods - "UpperVersion": "1.3", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/446", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Buy Cooking Recipes", "ID": "Denifia.BuyRecipes", - "UpperVersion": "1.0", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1126", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Buy Back Collectables", "ID": "BuyBackCollectables", - "UpperVersion": "1.3", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/507", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Chest Label System", "ID": "SPDChestLabel", - "UpperVersion": "1.6", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/242", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SDV 1.1." + "Compatibility": { + "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.1 + } }, { "Name": "Chest Pooling", "ID": "ChestPooling.dll", - "UpperVersion": "1.2", "UpdateUrls": [ "http://community.playstarbound.com/threads/111988" ], - "Notes": "Broke in SDV 1.2." + "Compatibility": { + "~1.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } }, { "Name": "Chests Anywhere", "ID": "ChestsAnywhere | Pathoschild.ChestsAnywhere", // changed in 1.9 - "UpperVersion": "1.9-beta", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/518" ], - "Notes": "Broke in SDV 1.2." + "Compatibility": { + "~1.9-beta": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } }, { "Name": "Choose Baby Gender", "ID": "ChooseBabyGender.dll", - "UpperVersion": "1.0.2", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/590", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.0.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "CJB Automation", "ID": "CJBAutomation", - "UpperVersion": "1.4", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/211", "http://www.nexusmods.com/stardewvalley/mods/1063" ], - "Notes": "Broke in SDV 1.2." + "Compatibility": { + "~1.4": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } }, { "Name": "CJB Cheats Menu", "ID": "CJBCheatsMenu", - "UpperVersion": "1.12", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/4" ], - "Notes": "Broke in SDV 1.1." + "Compatibility": { + "~1.12": { "Status": "AssumeBroken" } // broke in SDV 1.1 + } }, { "Name": "CJB Item Spawner", "ID": "CJBItemSpawner", - "UpperVersion": "1.5", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/93" ], - "Notes": "Broke in SDV 1.1." + "Compatibility": { + "~1.5": { "Status": "AssumeBroken" } // broke in SDV 1.1 + } }, { "Name": "CJB Show Item Sell Price", "ID": "CJBShowItemSellPrice", - "UpperVersion": "1.6", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/93" ], - "Notes": "Broke in SDV 1.2." + "Compatibility": { + "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } }, { "Name": "Climates of Ferngill", "ID": "KoihimeNakamura.ClimatesOfFerngill", - "UpperVersion": "1.0", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/604", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Cold Weather Haley", "ID": "LordXamon.ColdWeatherHaleyPRO", - "UpperVersion": "1.0", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1169", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Colored Chests", "ID": "4befde5c-731c-4853-8e4b-c5cdf946805f", - "Status": "Obsolete", - "ReasonPhrase": "colored chests were added in Stardew Valley 1.1." + "Compatibility": { + "~": { "Status": "Obsolete", "ReasonPhrase": "colored chests were added in Stardew Valley 1.1." } + } }, { "Name": "Combat with Farm Implements", "ID": "SPDFarmingImplementsInCombat", - "UpperVersion": "1.0", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/313", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Configurable Shipping Dates", "ID": "ConfigurableShippingDates", - "UpperVersion": "1.1.1", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/675", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Cooking Skill", "ID": "CookingSkill | spacechase0.CookingSkill", // changed in 1.0.4–6 - "UpperVersion": "1.0.6", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/522" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.0.6": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "CrabNet", "ID": "CrabNet.dll", - "UpperVersion": "1.0.4", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/584", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.0.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Customize Exterior", "ID": "CustomizeExterior", - "UpperVersion": "1.0.2", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1099" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.0.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Customizable Traveling Cart Days", "ID": "TravelingCartYyeahdude", - "UpperVersion": "1.0", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/567", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Daily News", "ID": "bashNinja.DailyNews", - "UpperVersion": "1.1", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1141", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Dynamic Checklist", "ID": "gunnargolf.DynamicChecklist", - "UpperVersion": "1.0", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1145", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Dynamic Machines", "ID": "DynamicMachines", - "UpperVersion": "1.1", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/374", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Empty Hands", "ID": "QuicksilverFox.EmptyHands", - "UpperVersion": "1.0", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1176", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Enemy Health Bars", "ID": "SPDHealthBar", - "UpperVersion": "1.7", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/193", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SDV 1.2." + "Compatibility": { + "~1.7": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } }, { "Name": "Entoarox Framework", "ID": "eacdb74b-4080-4452-b16b-93773cda5cf9 | Entoarox.EntoaroxFramework", // changed in ??? - "UpperVersion": "1.7.9", "UpdateUrls": [ "http://community.playstarbound.com/resources/4228" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.7.9": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Extended Fridge", "ID": "Mystra007ExtendedFridge", - "UpperVersion": "1.0", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/485", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SDV 1.2." + "Compatibility": { + "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } }, { "Name": "Extended Greenhouse", "ID": "ExtendedGreenhouse", - "UpperVersion": "1.0.2", "UpdateUrls": [ "http://community.playstarbound.com/resources/4303", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SDV 1.2." + "Compatibility": { + "~1.0.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } }, { "Name": "Fall 28 Snow Day", "ID": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Fall28 Snow Day'}", // disambiguate from other mods by Alpha_Omegasis - "UpperVersion": "1.3", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/486", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Farm Automation: Barn Door Automation", "ID": "FarmAutomation.BarnDoorAutomation.dll", - "UpperVersion": "1.0", "UpdateUrls": [ "http://community.playstarbound.com/threads/111931", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Farm Automation: Item Collector", "ID": "FarmAutomation.ItemCollector.dll", - "UpperVersion": "1.0", "UpdateUrls": [ "http://community.playstarbound.com/threads/111931", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SDV 1.2." + "Compatibility": { + "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } }, { "Name": "Farm Automation Unofficial: Item Collector", "ID": "Maddy99.FarmAutomation.ItemCollector", - "UpperVersion": "0.5", "UpdateUrls": [ "http://community.playstarbound.com/threads/125172", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~0.5": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Farm Expansion", "ID": "3888bdfd-73f6-4776-8bb7-8ad45aea1915 | AdvizeFarmExpansionMod-2-0 | AdvizeFarmExpansionMod-2-0-5", // changed in 2.0 and 2.0.5 - "UpperVersion": "2.0.5", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/130", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~2.0.5": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Farm Resource Generator", "ID": "FarmResourceGenerator.dll", - "UpperVersion": "1.0.4", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/647", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.0.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Faster Run", "ID": "FasterRun.dll", - "UpperVersion": "1.1", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/733", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "FlorenceMod", "ID": "FlorenceMod.dll", - "UpperVersion": "1.1", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/591", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Forage at the Farm", "ID": "ForageAtTheFarm", - "UpperVersion": "1.5.1", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/673", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.5.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Instant Geode", "ID": "InstantGeode", - "UpperVersion": "1.12", "UpdateUrls": [ "http://community.playstarbound.com/threads/109038", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SDV 1.2." + "Compatibility": { + "~1.12": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } }, { "Name": "Gate Opener", "ID": "GateOpener.dll | mralbobo.GateOpener", // changed in 1.1 - "UpperVersion": "1.0.1", "UpdateUrls": [ "http://community.playstarbound.com/threads/111988" ], - "Notes": "Broke in SDV 1.2." + "Compatibility": { + "~1.0.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } }, { "Name": "GenericShopExtender", "ID": "GenericShopExtender", - "UpperVersion": "0.1.2", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/814", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~0.1.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Get Dressed", "ID": "GetDressed.dll | Advize.GetDressed", // changed in 3.3 - "UpperVersion": "3.3", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/331" ], - "Notes": "Broke in SDV 1.2." + "Compatibility": { + "~3.3": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } }, { "Name": "Gift Taste Helper", "ID": "8008db57-fa67-4730-978e-34b37ef191d6", - "UpperVersion": "2.3.1", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/229" ], - "Notes": "Broke in SDV 1.2." + "Compatibility": { + "~2.3.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } }, { "Name": "Happy Animals", "ID": "HappyAnimals", - "UpperVersion": "1.0.3", "UpdateUrls": [ "https://community.playstarbound.com/threads/126655", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.0.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Happy Birthday", "ID": "{ID:'HappyBirthday', Author:'Alpha_Omegasis'}", // disambiguate from Oxyligen's fork - "UpperVersion": "1.3", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/520", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Harvest With Scythe", "ID": "965169fd-e1ed-47d0-9f12-b104535fb4bc", - "UpperVersion": "1.0.6", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/236", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.0.6": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Hunger for Food", "ID": "HungerForFoodByTigerle", - "UpperVersion": "0.1.2", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/810", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~0.1.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Improved Quality of Life", "ID": "Demiacle.ImprovedQualityOfLife", - "UpperVersion": "1.1", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1025", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Less Strict Over-Exertion (AntiExhaustion)", "ID": "BALANCEMOD_AntiExhaustion", - "UpperVersion": "1.1", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/637", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Lookup Anything", "ID": "LookupAnything | Pathoschild.LookupAnything", // changed in 1.10.1 - "UpperVersion": "1.10.1", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/541" ], - "Notes": "Broke in SDV 1.2." + "Compatibility": { + "~1.10.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } }, { "Name": "Loved Labels", "ID": "LovedLabels.dll", - "UpperVersion": "2.0", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/279" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~2.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Luck Skill", "ID": "LuckSkill | spacechase0.LuckSkill", // changed in 0.1.4 - "UpperVersion": "0.1.4", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/521" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~0.1.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "MailOrderPigs", "ID": "MailOrderPigs.dll", - "UpperVersion": "1.0.1", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/632", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.0.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Makeshift Multiplayer", "ID": "StardewValleyMP | spacechase0.StardewValleyMP", // changed in 0.3 - "UpperVersion": "0.3.6", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/501", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~0.3.6": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Message Box [API]? (ChatMod)", "ID": "Kithio:ChatMod", - "UpperVersion": "1.0", "UpdateUrls": [ "http://community.playstarbound.com/resources/message-box-api.4296", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Modder Serialization Utility", "ID": "SerializerUtils-0-1", - "Status": "Obsolete", - "ReasonPhrase": "it's no longer maintained or used." + "Compatibility": { + "~": { "Status": "Obsolete", "ReasonPhrase": "it's no longer maintained or used." } + } }, { "Name": "More Artifact Spots", "ID": "451", - "UpperVersion": "1.0.1", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/451", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.0.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "More Pets", "ID": "821ce8f6-e629-41ad-9fde-03b54f68b0b6MOREPETS | Entoarox.MorePets", // changed in 1.3 - "UpperVersion": "1.3.2", "UpdateUrls": [ "http://community.playstarbound.com/resources/4288" ], - "Notes": "Overhauled for SMAPI 1.11+ compatibility." + "Compatibility": { + "~1.3.2": { "Status": "AssumeBroken" } // overhauled for SMAPI 1.11+ compatibility + } }, { "Name": "More Rain", "ID": "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'More_Rain'}", // disambiguate from other mods by Alpha_Omegasis - "UpperVersion": "1.4", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/441", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Multiple Sprites and Portraits On Rotation (File Loading)", "ID": "FileLoading", - "UpperVersion": "1.12", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1094", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.12": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Museum Rearranger", "ID": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Museum Rearranger'}", // disambiguate from other mods by Alpha_Omegasis - "UpperVersion": "1.3", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/428", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "New Machines", "ID": "F70D4FAB-0AB2-4B78-9F1B-AF2CA2236A59", - "UpperVersion": "4.2.1343", "UpdateUrls": [ "http://community.playstarbound.com/resources/3683", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~4.2.1343": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Night Owl", "ID": "{ID:'SaveAnywhere', Name:'Stardew_NightOwl'}", // disambiguate from Save Anywhere - "UpperVersion": "2.1", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/433", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~2.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "No Debug Mode", "ID": "NoDebugMode", - "Status": "Obsolete", - "ReasonPhrase": "debug mode was removed in SMAPI 1.0." + "Compatibility": { + "~": { "Status": "Obsolete", "ReasonPhrase": "debug mode was removed in SMAPI 1.0." } + } }, { "Name": "NoSoilDecay", "ID": "289dee03-5f38-4d8e-8ffc-e440198e8610", - "UpperVersion": "0.5", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/237", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SDV 1.2, and uses Assembly.GetExecutingAssembly().Location." + "Compatibility": { + "~0.5": { "Status": "AssumeBroken" } // broke in SDV 1.2, and uses Assembly.GetExecutingAssembly().Location + } }, { "Name": "NPC Map Locations", "ID": "NPCMapLocationsMod", - "LowerVersion": "1.42", - "UpperVersion": "1.43", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/239" ], - "ReasonPhrase": "this version has an update check error which crashes the game." + "Compatibility": { + "1.42~1.43": { "Status": "AssumeBroken", "ReasonPhrase": "this version has an update check error which crashes the game." } + } }, { "Name": "NPC Speak", "ID": "NpcEcho.dll", - "UpperVersion": "1.0", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/694", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0" + "Compatibility": { + "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "OmniFarm", "ID": "BlueMod_OmniFarm", - "UpperVersion": "2.0.1", "UpdateUrls": [ "http://community.playstarbound.com/threads/127299", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0" + "Compatibility": { + "~2.0.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Part of the Community", "ID": "SB_PotC", - "UpperVersion": "1.0.8", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/923" ], - "Notes": "Broke in SDV 1.2." + "Compatibility": { + "~1.0.8": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } }, { "Name": "PelicanFiber", "ID": "PelicanFiber.dll", - "UpperVersion": "3.0", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/631", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~3.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "PelicanTTS", "ID": "Platonymous.PelicanTTS", - "UpperVersion": "1.6", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1079", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.6": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Persival's BundleMod", "ID": "BundleMod.dll", - "UpperVersion": "1.0", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/438", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SDV 1.1." + "Compatibility": { + "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.1 + } }, { "Name": "Point-and-Plant", "ID": "PointAndPlant.dll", - "UpperVersion": "1.0.2", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/572", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SDV 1.2." + "Compatibility": { + "~1.0.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } }, { "Name": "PrairieKingMadeEasy", "ID": "PrairieKingMadeEasy.dll", - "UpperVersion": "1.0", "UpdateUrls": [ "http://community.playstarbound.com/resources/3594", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SDV 1.2." + "Compatibility": { + "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } }, { "Name": "RainRandomizer", "ID": "RainRandomizer.dll", - "UpperVersion": "1.0.3", "UpdateUrls": [ "http://community.playstarbound.com/threads/111526", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.0.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "RelationshipsEnhanced", "ID": "relationshipsenhanced", - "UpperVersion": "1.0", "UpdateUrls": [ "http://community.playstarbound.com/resources/4435", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "RelationShipStatus", "ID": "relationshipstatus", - "UpperVersion": "1.0.5", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/751", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.0.5": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Replanter", "ID": "Replanter.dll", - "UpperVersion": "1.0.4", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/589", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.0.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Reusable Wallpapers and Floors (Wallpaper Retain)", "ID": "dae1b553-2e39-43e7-8400-c7c5c836134b", - "UpperVersion": "1.5", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/356", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.5": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Rush Orders", "ID": "RushOrders | spacechase0.RushOrders", // changed in 1.1 - "UpperVersion": "1.1", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/605" ], - "Notes": "Broke in SDV 1.2." + "Compatibility": { + "~1.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } }, { "Name": "Save Anywhere", "ID": "{ID:'SaveAnywhere', Name:'Save Anywhere'}", // disambiguate from Night Owl - "UpperVersion": "2.4", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/444", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~2.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Seasonal Immersion", "ID": "EntoaroxSeasonalHouse | EntoaroxSeasonalBuildings | EntoaroxSeasonalImmersion | Entoarox.SeasonalImmersion", // changed in 1.1, 1.6 or earlier, and 1.7 - "UpperVersion": "1.8.2", "UpdateUrls": [ "http://community.playstarbound.com/resources/4262", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.8.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Send Items", "ID": "Denifia.SendItems", - "UpperVersion": "1.0.2", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1087", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.0.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Shed Notifications (BuildingsNotifications)", "ID": "TheCroak.BuildingsNotifications", - "UpperVersion": "0.4.1", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/620", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~0.4.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Shenandoah Project", "ID": "Shenandoah Project", - "UpperVersion": "1.1.1", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/756", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Shipment Tracker", "ID": "7e474181-e1a0-40f9-9c11-d08a3dcefaf3", - "UpperVersion": "1.1", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/321", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Shop Expander", "ID": "821ce8f6-e629-41ad-9fde-03b54f68b0b6 | EntoaroxShopExpander | Entoarox.ShopExpander", // changed in 1.5 and 1.5.2 - "UpperVersion": "1.5.3", "UpdateUrls": [ "http://community.playstarbound.com/resources/4381", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.5.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Showcase Mod", "ID": "Igorious.Showcase", - "UpperVersion": "0.9", "UpdateUrls": [ "http://community.playstarbound.com/resources/4487", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~0.9": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Simple Sprinklers", "ID": "SimpleSprinkler.dll", - "UpperVersion": "1.4", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/76", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SDV 1.2." + "Compatibility": { + "~1.4": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } }, { "Name": "Siv's Marriage Mod", "ID": "6266959802", - "UpperVersion": "1.2.2", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/366", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 1.9 (has multiple Mod instances)." + "Compatibility": { + "~1.2.2": { "Status": "AssumeBroken" } // broke in SMAPI 1.9 (has multiple Mod instances) + } }, { "Name": "Skill Prestige", "ID": "6b843e60-c8fc-4a25-a67b-4a38ac8dcf9b", - "UpperVersion": "1.0.9", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/569", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.0.9": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Skill Prestige: Cooking Adapter", "ID": "20d6b8a3-b6e7-460b-a6e4-07c2b0cb6c63", - "UpperVersion": "1.0.9", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/569", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.0.9": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Slower Fence Decay", "ID": "SPDSlowFenceDecay", - "UpperVersion": "0.5.1", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/252", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~0.5.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Smart Mod", "ID": "KuroBear.SmartMod", - "UpperVersion": "2.2", "UpdateUrls": [ "http://community.playstarbound.com/threads/108104", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~2.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Solar Eclipse Event", "ID": "KoihimeNakamura.SolarEclipseEvent", - "UpperVersion": "1.1", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/897", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Sprinkles", "ID": "Platonymous.Sprinkles", - "UpperVersion": "1.1.3", "UpdateUrls": [ "http://community.playstarbound.com/resources/4592", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Sprint and Dash", "ID": "SPDSprintAndDash", - "UpperVersion": "1.0", "UpdateUrls": [ "http://community.playstarbound.com/resources/3531", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SDV 1.2." + "Compatibility": { + "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } }, { "Name": "Sprint and Dash Redux", "ID": "SPDSprintAndDash", - "UpperVersion": "1.2", "UpdateUrls": [ "http://community.playstarbound.com/resources/4201" ], - "Notes": "Broke in SDV 1.2." + "Compatibility": { + "~1.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } }, { "Name": "Sprinting Mod", "ID": "a10d3097-b073-4185-98ba-76b586cba00c", - "UpperVersion": "2.1", "UpdateUrls": [ "http://community.playstarbound.com/threads/108313", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SDV 1.2." + "Compatibility": { + "~2.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } }, { "Name": "StackSplitX", "ID": "StackSplitX.dll", - "UpperVersion": "1.2", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/798" ], - "Notes": "Broke in SDV 1.2." + "Compatibility": { + "~1.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } }, { "Name": "StaminaRegen", "ID": "StaminaRegen.dll", - "UpperVersion": "1.0.3", "UpdateUrls": [ "http://community.playstarbound.com/threads/111526", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.0.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Stardew Auto Backup", "ID": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'Stardew_Save_Backup'}", // disambiguate from other Alpha_Omegasis mods - "UpperVersion": "1.2", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/435", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Stardew Notification", "ID": "stardewnotification", - "UpperVersion": "1.7", "UpdateUrls": [ "http://community.playstarbound.com/threads/127979", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.7": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Stardew Symphony", "ID": "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'Stardew_Symphony'}", // disambiguate other mods by Alpha_Omegasis - "UpperVersion": "1.3", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/425", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "StarDustCore", "ID": "StarDustCore", - "Status": "Obsolete", - "ReasonPhrase": "it was only used by earlier versions of Save Anywhere, and is no longer used or maintained." + "Compatibility": { + "~": { "Status": "Obsolete", "ReasonPhrase": "it was only used by earlier versions of Save Anywhere, and is no longer used or maintained." } + } }, { "Name": "StashItemsToChest", "ID": "BlueMod_StashItemsToChest", - "UpperVersion": "1.0.1", "UpdateUrls": [ "http://community.playstarbound.com/threads/126906", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.0.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Stone Bridge Over Pond (PondWithBridge)", "ID": "PondWithBridge.dll", - "UpperVersion": "1.0", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/316", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Super Greenhouse Warp Modifier", "ID": "SuperGreenhouse", - "UpperVersion": "1.0", - "UpperVersionLabel": "1.0 (2016-11-29)", "UpdateUrls": [ "http://community.playstarbound.com/resources/4334", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Tainted Cellar", "ID": "TaintedCellar.dll", - "UpperVersion": "1.0", "UpdateUrls": [ "http://community.playstarbound.com/threads/115735", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SDV 1.1 or 1.11." + "Compatibility": { + "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.1 or 1.11 + } }, { "Name": "Teleporter", "ID": "Teleporter", - "UpperVersion": "1.0.2", "UpdateUrls": [ "http://community.playstarbound.com/resources/4374", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SDV 1.2." + "Compatibility": { + "~1.0.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } }, { "Name": "Three-heart Dance Partner", "ID": "ThreeHeartDancePartner", - "UpperVersion": "1.0.1", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/500", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SDV 1.2." + "Compatibility": { + "~1.0.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } }, { "Name": "TimeSpeed", "ID": "TimeSpeed.dll | {ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'TimeSpeed'} | {ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'TimeSpeed Mod (unofficial)'} | community.TimeSpeed", // changed in 2.0.3 and 2.1; disambiguate other mods by Alpha_Omegasis - "UpperVersion": "2.2", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/169" ], - "Notes": "Broke in SDV 1.2." + "Compatibility": { + "~2.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } }, { "Name": "UiModSuite", "ID": "Demiacle.UiModSuite", - "UpperVersion": "1.0", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1023", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SDV 1.2." + "Compatibility": { + "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } }, { "Name": "WakeUp", "ID": "WakeUp.dll", - "UpperVersion": "1.0.2", "UpdateUrls": [ "http://community.playstarbound.com/threads/111526", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.0.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Wallpaper Fix", "ID": "WallpaperFix.dll", - "UpperVersion": "1.1", "UpdateUrls": [ "http://community.playstarbound.com/resources/4211", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "Weather Controller", "ID": "WeatherController.dll", - "UpperVersion": "1.0.2", "UpdateUrls": [ "http://community.playstarbound.com/threads/111526", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SDV 1.2." + "Compatibility": { + "~1.0.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } }, { "Name": "Wonderful Farm Life", "ID": "WonderfulFarmLife.dll", - "UpperVersion": "1.0", "UpdateUrls": [ "http://community.playstarbound.com/threads/115384", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SDV 1.1 or 1.11." + "Compatibility": { + "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.1 or 1.11 + } }, { "Name": "XmlSerializerRetool", "ID": "XmlSerializerRetool.dll", - "Status": "Obsolete", - "ReasonPhrase": "it's no longer maintained or used." + "Compatibility": { + "~": { "Status": "Obsolete", "ReasonPhrase": "it's no longer maintained or used." } + } }, { "Name": "Xnb Loader", "ID": "Entoarox.XnbLoader", - "UpperVersion": "1.0.6", "UpdateUrls": [ "http://community.playstarbound.com/resources/4506", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], - "Notes": "Broke in SMAPI 2.0." + "Compatibility": { + "~1.0.6": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { "Name": "zDailyIncrease", "ID": "zdailyincrease", - "UpperVersion": "1.2", "UpdateUrls": [ "http://community.playstarbound.com/resources/4247" ], - "Notes": "Broke in SDV 1.2." + "Compatibility": { + "~1.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } }, { "Name": "Zoom Out Extreme", "ID": "ZoomMod", - "UpperVersion": "0.1", "UpdateUrls": [ "http://community.playstarbound.com/threads/115028" ], - "Notes": "Broke in SDV 1.2." + "Compatibility": { + "~0.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } }, { "Name": "Zoryn's Better RNG", "ID": "76b6d1e1-f7ba-4d72-8c32-5a1e6d2716f6 | Zoryn.BetterRNG", // changed in 1.6 - "UpperVersion": "1.6", "UpdateUrls": [ "https://github.com/Zoryn4163/SMAPI-Mods/releases" ], - "Notes": "Broke in SDV 1.2." + "Compatibility": { + "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } }, { "Name": "Zoryn's Calendar Anywhere", "ID": "a41c01cd-0437-43eb-944f-78cb5a53002a | Zoryn.CalendarAnywhere", // changed in 1.6 - "UpperVersion": "1.6", "UpdateUrls": [ "https://github.com/Zoryn4163/SMAPI-Mods/releases" ], - "Notes": "Broke in SDV 1.2." + "Compatibility": { + "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } }, { "Name": "Zoryn's Health Bars", "ID": "HealthBars.dll | Zoryn.HealthBars", // changed in 1.6 - "UpperVersion": "1.6", "UpdateUrls": [ "https://github.com/Zoryn4163/SMAPI-Mods/releases" ], - "Notes": "Broke in SDV 1.2." + "Compatibility": { + "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } }, { "Name": "Zoryn's Junimo Deposit Anywhere", "ID": "f93a4fe8-cade-4146-9335-b5f82fbbf7bc | Zoryn.JunimoDepositAnywhere", // changed in 1.6 - "UpperVersion": "1.7", "UpdateUrls": [ "https://github.com/Zoryn4163/SMAPI-Mods/releases" ], - "Notes": "Broke in SDV 1.2." + "Compatibility": { + "~1.7": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } }, { "Name": "Zoryn's Movement Mod", "ID": "8a632929-8335-484f-87dd-c29d2ba3215d | Zoryn.MovementModifier", // changed in 1.6 - "UpperVersion": "1.6", "UpdateUrls": [ "https://github.com/Zoryn4163/SMAPI-Mods/releases" ], - "Notes": "Broke in SDV 1.2." + "Compatibility": { + "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } }, { "Name": "Zoryn's Regen Mod", "ID": "dfac4383-1b6b-4f33-ae4e-37fc23e5252e | Zoryn.RegenMod", // changed in 1.6 - "UpperVersion": "1.6", "UpdateUrls": [ "https://github.com/Zoryn4163/SMAPI-Mods/releases" ], - "Notes": "Broke in SDV 1.2." + "Compatibility": { + "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } } ] } diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 38064f98..8863590b 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -90,6 +90,7 @@ Properties\GlobalAssemblyInfo.cs + From d3f0c8e4d2d9ada099cba67c359c5df1d69a1257 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 Sep 2017 01:10:17 -0400 Subject: [PATCH 100/186] add support for update checks from the Chucklefish mod site (#336) --- release-notes.md | 2 +- .../Controllers/ModsController.cs | 11 ++- .../ConfigModels/ModUpdateCheckConfig.cs | 18 +++- .../ModRepositories/ChucklefishRepository.cs | 94 +++++++++++++++++++ .../ModRepositories/GitHubRepository.cs | 4 +- .../StardewModdingAPI.Web.csproj | 1 + src/StardewModdingAPI.Web/appsettings.json | 7 +- .../Framework/Models/Manifest.cs | 3 + src/StardewModdingAPI/IManifest.cs | 7 +- src/StardewModdingAPI/Program.cs | 2 + 10 files changed, 140 insertions(+), 9 deletions(-) create mode 100644 src/StardewModdingAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs diff --git a/release-notes.md b/release-notes.md index a07de71f..41d946c7 100644 --- a/release-notes.md +++ b/release-notes.md @@ -14,7 +14,7 @@ For players: For mod developers: * Added new APIs to edit, inject, and reload XNB assets loaded by the game at any time. _This let mods do anything previously only possible with XNB mods, plus enables new mod scenarios (e.g. seasonal textures, NPC clothing that depend on the weather or location, etc)._ -* Added new manifest fields to enable automatic update checks. +* Added support for automatic update checks from Chucklefish, GitHub, or Nexus Mods. * Added new input events. _The new `InputEvents` combine keyboard + mouse + controller input into one event for easy handling, add metadata like the cursor position and grab tile to support click handling, and add an option to suppress input from the game to enable new scenarios like action highjacking and UI overlays._ * Added support for optional dependencies. diff --git a/src/StardewModdingAPI.Web/Controllers/ModsController.cs b/src/StardewModdingAPI.Web/Controllers/ModsController.cs index 8fc2cb51..c5c79600 100644 --- a/src/StardewModdingAPI.Web/Controllers/ModsController.cs +++ b/src/StardewModdingAPI.Web/Controllers/ModsController.cs @@ -5,9 +5,9 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; +using StardewModdingAPI.Models; using StardewModdingAPI.Web.Framework.ConfigModels; using StardewModdingAPI.Web.Framework.ModRepositories; -using StardewModdingAPI.Models; namespace StardewModdingAPI.Web.Controllers { @@ -41,14 +41,21 @@ namespace StardewModdingAPI.Web.Controllers this.Cache = cache; this.CacheMinutes = config.CacheMinutes; + string version = this.GetType().Assembly.GetName().Version.ToString(3); this.Repositories = new IModRepository[] { + new ChucklefishRepository( + vendorKey: config.ChucklefishKey, + userAgent: string.Format(config.ChucklefishUserAgent, version), + baseUrl: config.ChucklefishBaseUrl, + modPageUrlFormat: config.ChucklefishModPageUrlFormat + ), new GitHubRepository( vendorKey: config.GitHubKey, baseUrl: config.GitHubBaseUrl, releaseUrlFormat: config.GitHubReleaseUrlFormat, - userAgent: config.GitHubUserAgent, + userAgent: string.Format(config.GitHubUserAgent, version), acceptHeader: config.GitHubAcceptHeader, username: config.GitHubUsername, password: config.GitHubPassword diff --git a/src/StardewModdingAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs b/src/StardewModdingAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs index 5d55ba18..8ca555a3 100644 --- a/src/StardewModdingAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs +++ b/src/StardewModdingAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs @@ -12,13 +12,29 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels /// The number of minutes update checks should be cached before refetching them. public int CacheMinutes { get; set; } + /**** + ** Chucklefish mod site + ****/ + /// The repository key for the Chucklefish mod site. + public string ChucklefishKey { get; set; } + + /// The user agent for the Chucklefish API client, where {0} is the SMAPI version. + public string ChucklefishUserAgent { get; set; } + + /// The base URL for the Chucklefish mod site. + public string ChucklefishBaseUrl { get; set; } + + /// The URL for a mod page on the Chucklefish mod site excluding the , where {0} is the mod ID. + public string ChucklefishModPageUrlFormat { get; set; } + + /**** ** GitHub ****/ /// The repository key for Nexus Mods. public string GitHubKey { get; set; } - /// The user agent for the GitHub API client. + /// The user agent for the GitHub API client, where {0} is the SMAPI version. public string GitHubUserAgent { get; set; } /// The base URL for the GitHub API. diff --git a/src/StardewModdingAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs b/src/StardewModdingAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs new file mode 100644 index 00000000..59d7f3ba --- /dev/null +++ b/src/StardewModdingAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs @@ -0,0 +1,94 @@ +using System; +using System.Net; +using System.Threading.Tasks; +using HtmlAgilityPack; +using Pathoschild.Http.Client; +using StardewModdingAPI.Models; + +namespace StardewModdingAPI.Web.Framework.ModRepositories +{ + /// An HTTP client for fetching mod metadata from the Chucklefish mod site. + internal class ChucklefishRepository : IModRepository + { + /********* + ** Properties + *********/ + /// The underlying HTTP client. + private readonly IClient Client; + + + /********* + ** Accessors + *********/ + /// The unique key for this vendor. + public string VendorKey { get; } + + /// The base URL for the Chucklefish mod site. + public string BaseUrl { get; } + + /// The URL for a mod page excluding the base URL, where {0} is the mod ID. + public string ModPageUrlFormat { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The unique key for this vendor. + /// The user agent for the API client. + /// The base URL for the Chucklefish mod site. + /// The URL for a mod page excluding the , where {0} is the mod ID. + public ChucklefishRepository(string vendorKey, string userAgent, string baseUrl, string modPageUrlFormat) + { + this.VendorKey = vendorKey; + this.BaseUrl = baseUrl; + this.ModPageUrlFormat = modPageUrlFormat; + this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent); + } + + /// Get metadata about a mod in the repository. + /// The mod ID in this repository. + public async Task GetModInfoAsync(string id) + { + try + { + // fetch HTML + string html; + try + { + html = await this.Client + .GetAsync(string.Format(this.ModPageUrlFormat, id)) + .AsString(); + } + catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound) + { + return new ModInfoModel("Found no mod with this ID."); + } + + // parse HTML + var doc = new HtmlDocument(); + doc.LoadHtml(html); + + // extract mod info + string url = new UriBuilder(new Uri(this.BaseUrl)) { Path = string.Format(this.ModPageUrlFormat, id) }.Uri.ToString(); + string name = doc.DocumentNode.SelectSingleNode("//meta[@name='twitter:title']").Attributes["content"].Value; + if (name.StartsWith("[SMAPI] ")) + name = name.Substring("[SMAPI] ".Length); + string version = doc.DocumentNode.SelectSingleNode("//h1/span").InnerText; + + // create model + return new ModInfoModel(name, version, url); + } + catch (Exception ex) + { + return new ModInfoModel(ex.ToString()); + } + } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public void Dispose() + { + this.Client.Dispose(); + } + } +} diff --git a/src/StardewModdingAPI.Web/Framework/ModRepositories/GitHubRepository.cs b/src/StardewModdingAPI.Web/Framework/ModRepositories/GitHubRepository.cs index 421220de..b08e8b4d 100644 --- a/src/StardewModdingAPI.Web/Framework/ModRepositories/GitHubRepository.cs +++ b/src/StardewModdingAPI.Web/Framework/ModRepositories/GitHubRepository.cs @@ -33,7 +33,7 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories /// The unique key for this vendor. /// The base URL for the Nexus Mods API. /// The URL for a Nexus Mods API query excluding the , where {0} is the mod ID. - /// The user agent for the GitHub API client. + /// The user agent for the API client. /// The Accept header value expected by the GitHub API. /// The username with which to authenticate to the GitHub API. /// The password with which to authenticate to the GitHub API. @@ -43,7 +43,7 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories this.ReleaseUrlFormat = releaseUrlFormat; this.Client = new FluentClient(baseUrl) - .SetUserAgent(string.Format(userAgent, this.GetType().Assembly.GetName().Version)) + .SetUserAgent(userAgent) .AddDefault(req => req.WithHeader("Accept", acceptHeader)); if (!string.IsNullOrWhiteSpace(username)) this.Client = this.Client.SetBasicAuthentication(username, password); diff --git a/src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj b/src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj index c30abc55..bf67449b 100644 --- a/src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj +++ b/src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj @@ -5,6 +5,7 @@ + diff --git a/src/StardewModdingAPI.Web/appsettings.json b/src/StardewModdingAPI.Web/appsettings.json index 29fb195e..a0d4d078 100644 --- a/src/StardewModdingAPI.Web/appsettings.json +++ b/src/StardewModdingAPI.Web/appsettings.json @@ -8,6 +8,11 @@ "ModUpdateCheck": { "CacheMinutes": 60, + "ChucklefishKey": "Chucklefish", + "ChucklefishUserAgent": "SMAPI/{0} (+https://github.com/Pathoschild/SMAPI)", + "ChucklefishBaseUrl": "https://community.playstarbound.com", + "ChucklefishModPageUrlFormat": "resources/{0}", + "GitHubKey": "GitHub", "GitHubUserAgent": "SMAPI/{0} (+https://github.com/Pathoschild/SMAPI)", "GitHubBaseUrl": "https://api.github.com", @@ -20,5 +25,5 @@ "NexusUserAgent": "Nexus Client v0.63.15", "NexusBaseUrl": "http://www.nexusmods.com/stardewvalley", "NexusModUrlFormat": "mods/{0}" - } + } } diff --git a/src/StardewModdingAPI/Framework/Models/Manifest.cs b/src/StardewModdingAPI/Framework/Models/Manifest.cs index 2e9566bf..c891644f 100644 --- a/src/StardewModdingAPI/Framework/Models/Manifest.cs +++ b/src/StardewModdingAPI/Framework/Models/Manifest.cs @@ -34,6 +34,9 @@ namespace StardewModdingAPI.Framework.Models [JsonConverter(typeof(SFieldConverter))] public IManifestDependency[] Dependencies { get; set; } + /// The mod's unique ID in the Chucklefish mod site (if any), used for update checks. + public string ChucklefishID { get; set; } + /// The mod's unique ID in Nexus Mods (if any), used for update checks. public string NexusID { get; set; } diff --git a/src/StardewModdingAPI/IManifest.cs b/src/StardewModdingAPI/IManifest.cs index 28f6570c..9513834a 100644 --- a/src/StardewModdingAPI/IManifest.cs +++ b/src/StardewModdingAPI/IManifest.cs @@ -32,11 +32,14 @@ namespace StardewModdingAPI /// The other mods that must be loaded before this mod. IManifestDependency[] Dependencies { get; } + /// The mod's unique ID in the Chucklefish mod site (if any), used for update checks. + string ChucklefishID { get; } + /// The mod's unique ID in Nexus Mods (if any), used for update checks. - string NexusID { get; set; } + string NexusID { get; } /// The mod's organisation and project name on GitHub (if any), used for update checks. - string GitHubProject { get; set; } + string GitHubProject { get; } /// Any manifest fields which didn't match a valid field. IDictionary ExtraFields { get; } diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 4860968c..f821b559 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -522,6 +522,8 @@ namespace StardewModdingAPI IDictionary modsByKey = new Dictionary(StringComparer.InvariantCultureIgnoreCase); foreach (IModMetadata mod in mods) { + if (!string.IsNullOrWhiteSpace(mod.Manifest.ChucklefishID)) + modsByKey[$"Chucklefish:{mod.Manifest.ChucklefishID}"] = mod; if (!string.IsNullOrWhiteSpace(mod.Manifest.NexusID)) modsByKey[$"Nexus:{mod.Manifest.NexusID}"] = mod; if (!string.IsNullOrWhiteSpace(mod.Manifest.GitHubProject)) From 00957a23177f792d4e4962854697779831d51ca1 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 Sep 2017 01:30:28 -0400 Subject: [PATCH 101/186] validate semantic versions in API (#336, #361) --- .../Controllers/ModsController.cs | 10 +++++++++- .../Framework/ConfigModels/ModUpdateCheckConfig.cs | 4 ++++ src/StardewModdingAPI.Web/appsettings.json | 1 + 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/StardewModdingAPI.Web/Controllers/ModsController.cs b/src/StardewModdingAPI.Web/Controllers/ModsController.cs index c5c79600..4eaa66d2 100644 --- a/src/StardewModdingAPI.Web/Controllers/ModsController.cs +++ b/src/StardewModdingAPI.Web/Controllers/ModsController.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; @@ -27,6 +28,9 @@ namespace StardewModdingAPI.Web.Controllers /// The number of minutes update checks should be cached before refetching them. private readonly int CacheMinutes; + /// A regex which matches SMAPI-style semantic version. + private readonly string VersionRegex; + /********* ** Public methods @@ -40,6 +44,7 @@ namespace StardewModdingAPI.Web.Controllers this.Cache = cache; this.CacheMinutes = config.CacheMinutes; + this.VersionRegex = config.SemanticVersionRegex; string version = this.GetType().Assembly.GetName().Version.ToString(3); this.Repositories = @@ -103,7 +108,10 @@ namespace StardewModdingAPI.Web.Controllers result[modKey] = await this.Cache.GetOrCreateAsync($"{repository.VendorKey}:{modID}".ToLower(), async entry => { entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(this.CacheMinutes); - return await repository.GetModInfoAsync(modID); + ModInfoModel info = await repository.GetModInfoAsync(modID); + if (info.Error == null && !Regex.IsMatch(info.Version, this.VersionRegex, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)) + info = new ModInfoModel(info.Name, info.Version, info.Url, $"Mod has invalid semantic version '{info.Version}'."); + return info; }); } diff --git a/src/StardewModdingAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs b/src/StardewModdingAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs index 8ca555a3..03de639e 100644 --- a/src/StardewModdingAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs +++ b/src/StardewModdingAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs @@ -12,6 +12,10 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels /// The number of minutes update checks should be cached before refetching them. public int CacheMinutes { get; set; } + /// A regex which matches SMAPI-style semantic version. + /// Derived from SMAPI's SemanticVersion implementation. + public string SemanticVersionRegex { get; set; } + /**** ** Chucklefish mod site ****/ diff --git a/src/StardewModdingAPI.Web/appsettings.json b/src/StardewModdingAPI.Web/appsettings.json index a0d4d078..852f6f71 100644 --- a/src/StardewModdingAPI.Web/appsettings.json +++ b/src/StardewModdingAPI.Web/appsettings.json @@ -7,6 +7,7 @@ }, "ModUpdateCheck": { "CacheMinutes": 60, + "SemanticVersionRegex": "^(?>(?0|[1-9]\\d*))\\.(?>(?0|[1-9]\\d*))(?>(?:\\.(?0|[1-9]\\d*))?)(?:-(?(?>[a-z0-9]+[\\-\\.]?)+))?$", "ChucklefishKey": "Chucklefish", "ChucklefishUserAgent": "SMAPI/{0} (+https://github.com/Pathoschild/SMAPI)", From 5f85d89974c3f1974f8e3d372f12b1cac743cd3a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 Sep 2017 02:15:28 -0400 Subject: [PATCH 102/186] handle common 'v' version prefix on GitHub (#336) --- .../Framework/ModRepositories/GitHubRepository.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/StardewModdingAPI.Web/Framework/ModRepositories/GitHubRepository.cs b/src/StardewModdingAPI.Web/Framework/ModRepositories/GitHubRepository.cs index b08e8b4d..f794c605 100644 --- a/src/StardewModdingAPI.Web/Framework/ModRepositories/GitHubRepository.cs +++ b/src/StardewModdingAPI.Web/Framework/ModRepositories/GitHubRepository.cs @@ -55,11 +55,19 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories { try { + // fetch data GitRelease release = await this.Client .GetAsync(string.Format(this.ReleaseUrlFormat, id)) .As(); - return new ModInfoModel(id, release.Tag, $"https://github.com/{id}/releases"); + // extract fields + string name = id; + string version = release.Tag; + if (version.StartsWith("v")) // common format on GitHub + version = version.Substring(1); + string url = $"https://github.com/{id}/releases"; + + return new ModInfoModel(name, version, url); } catch (Exception ex) { From 8e0d1b8682f0898e941fee16c6649f1165fde499 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 Sep 2017 02:25:35 -0400 Subject: [PATCH 103/186] minor bug fixes (#361) --- .../Framework/Serialisation/SFieldConverter.cs | 2 +- src/StardewModdingAPI/Program.cs | 2 +- src/StardewModdingAPI/StardewModdingAPI.config.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/StardewModdingAPI/Framework/Serialisation/SFieldConverter.cs b/src/StardewModdingAPI/Framework/Serialisation/SFieldConverter.cs index ffece081..917c950d 100644 --- a/src/StardewModdingAPI/Framework/Serialisation/SFieldConverter.cs +++ b/src/StardewModdingAPI/Framework/Serialisation/SFieldConverter.cs @@ -97,7 +97,7 @@ namespace StardewModdingAPI.Framework.Serialisation foreach (JProperty property in JObject.Load(reader).Properties()) { string range = property.Name; - ModStatus status = property.Value.Value(nameof(ModCompatibility.Status)); + ModStatus status = (ModStatus)Enum.Parse(typeof(ModStatus), property.Value.Value(nameof(ModCompatibility.Status))); string reasonPhrase = property.Value.Value(nameof(ModCompatibility.ReasonPhrase)); result.Add(new ModCompatibility(range, status, reasonPhrase)); diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index f821b559..fcfa1efc 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -642,7 +642,7 @@ namespace StardewModdingAPI Assembly modAssembly; try { - modAssembly = modAssemblyLoader.Load(metadata, assemblyPath, assumeCompatible: metadata.DataRecord.GetCompatibility(metadata.Manifest.Version)?.Status == ModStatus.AssumeCompatible); + modAssembly = modAssemblyLoader.Load(metadata, assemblyPath, assumeCompatible: metadata.DataRecord?.GetCompatibility(metadata.Manifest.Version)?.Status == ModStatus.AssumeCompatible); } catch (IncompatibleInstructionException ex) { diff --git a/src/StardewModdingAPI/StardewModdingAPI.config.json b/src/StardewModdingAPI/StardewModdingAPI.config.json index 3844b8b0..75b884f5 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.config.json +++ b/src/StardewModdingAPI/StardewModdingAPI.config.json @@ -58,7 +58,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": "1e36d4ca-c7ef-4dfb-9927-d27a6c3c8bdc", "UpdateUrls": [ "http://community.playstarbound.com/resources/3532", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Compatibility": { - "~0.01": { "Status": "AssumeBroken" } // broke in SMAPI 1.9 + "~0.1": { "Status": "AssumeBroken" } // broke in SMAPI 1.9 } }, { From 0c06b129cadd17ba1e9533319326d8aac8a88ed6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 Sep 2017 02:33:33 -0400 Subject: [PATCH 104/186] add support for specifying default update fields, migrate mods already in mod list (#361) --- .../Core/ModResolverTests.cs | 2 +- .../Framework/ModLoading/ModResolver.cs | 29 +- .../Framework/Models/ModDataDefaults.cs | 18 + .../Framework/Models/ModDataRecord.cs | 7 +- .../StardewModdingAPI.config.json | 335 +++++++++++------- .../StardewModdingAPI.csproj | 1 + 6 files changed, 253 insertions(+), 139 deletions(-) create mode 100644 src/StardewModdingAPI/Framework/Models/ModDataDefaults.cs diff --git a/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs b/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs index 2d65eee9..6a3fded6 100644 --- a/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs +++ b/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs @@ -144,7 +144,7 @@ namespace StardewModdingAPI.Tests.Core this.SetupMetadataForValidation(mock, new ModDataRecord { Compatibility = new[] { new ModCompatibility("~1.0", ModStatus.AssumeBroken, null) }, - UpdateUrls = new[] { "http://example.org" } + AlternativeUrl = "http://example.org" }); // act diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs index 31ba5bc5..4d20e7c8 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs @@ -53,17 +53,22 @@ namespace StardewModdingAPI.Framework.ModLoading error = $"parsing its manifest failed:\n{ex.GetLogSummary()}"; } - // validate metadata + // get internal data record (if any) ModDataRecord dataRecord = null; if (manifest != null) { - // get unique key for lookups string key = !string.IsNullOrWhiteSpace(manifest.UniqueID) ? manifest.UniqueID : manifest.EntryDll; - - // get data record dataRecord = dataRecords.FirstOrDefault(record => record.ID.Matches(key, manifest)); } + // apply defaults + if (dataRecord?.Defaults != null) + { + manifest.ChucklefishID = manifest.ChucklefishID ?? dataRecord.Defaults.ChucklefishID; + manifest.GitHubProject = manifest.GitHubProject ?? dataRecord.Defaults.GitHubProject; + manifest.NexusID = manifest.NexusID ?? dataRecord.Defaults.NexusID; + } + // build metadata string displayName = !string.IsNullOrWhiteSpace(manifest?.Name) ? manifest.Name @@ -100,13 +105,27 @@ namespace StardewModdingAPI.Framework.ModLoading case ModStatus.AssumeBroken: { + // get reason string reasonPhrase = compatibility.ReasonPhrase ?? "it's no longer compatible"; + + // get update URLs + List updateUrls = new List(); + if (!string.IsNullOrWhiteSpace(mod.Manifest.ChucklefishID)) + updateUrls.Add($"https://community.playstarbound.com/resources/{mod.Manifest.ChucklefishID}"); + if (!string.IsNullOrWhiteSpace(mod.Manifest.NexusID)) + updateUrls.Add($"http://nexusmods.com/stardewvalley/mods/{mod.Manifest.NexusID}"); + if (!string.IsNullOrWhiteSpace(mod.Manifest.GitHubProject)) + updateUrls.Add($"https://github.com/{mod.Manifest.GitHubProject}/releases"); + if (mod.DataRecord.AlternativeUrl != null) + updateUrls.Add(mod.DataRecord.AlternativeUrl); + + // build error string error = $"{reasonPhrase}. Please check for a "; if (mod.Manifest.Version.Equals(compatibility.UpperVersion)) error += "newer version"; else error += $"version newer than {compatibility.UpperVersion}"; - error += " at " + string.Join(" or ", mod.DataRecord.UpdateUrls); + error += " at " + string.Join(" or ", updateUrls); mod.SetStatus(ModMetadataStatus.Failed, error); } diff --git a/src/StardewModdingAPI/Framework/Models/ModDataDefaults.cs b/src/StardewModdingAPI/Framework/Models/ModDataDefaults.cs new file mode 100644 index 00000000..e0ab94b8 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Models/ModDataDefaults.cs @@ -0,0 +1,18 @@ +namespace StardewModdingAPI.Framework.Models +{ + /// Default values for support fields to inject into the manifest. + internal class ModDataDefaults + { + /********* + ** Accessors + *********/ + /// The mod's unique ID in the Chucklefish mod site (if any), used for update checks. + public string ChucklefishID { get; set; } + + /// The mod's unique ID in Nexus Mods (if any), used for update checks. + public string NexusID { get; set; } + + /// The mod's organisation and project name on GitHub (if any), used for update checks. + public string GitHubProject { get; set; } + } +} diff --git a/src/StardewModdingAPI/Framework/Models/ModDataRecord.cs b/src/StardewModdingAPI/Framework/Models/ModDataRecord.cs index de219076..9f19d5f0 100644 --- a/src/StardewModdingAPI/Framework/Models/ModDataRecord.cs +++ b/src/StardewModdingAPI/Framework/Models/ModDataRecord.cs @@ -17,8 +17,11 @@ namespace StardewModdingAPI.Framework.Models /// The mod name. public string Name { get; set; } - /// The URLs the user can check for a newer version. - public string[] UpdateUrls { get; set; } + /// Default values for support fields to inject into the manifest. + public ModDataDefaults Defaults { get; set; } + + /// The URL where the player can get an unofficial or alternative version of the mod if the official version isn't compatible. + public string AlternativeUrl { get; set; } /// The compatibility of given mod versions (if any). [JsonConverter(typeof(SFieldConverter))] diff --git a/src/StardewModdingAPI/StardewModdingAPI.config.json b/src/StardewModdingAPI/StardewModdingAPI.config.json index 75b884f5..d2d28625 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.config.json +++ b/src/StardewModdingAPI/StardewModdingAPI.config.json @@ -48,7 +48,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "AccessChestAnywhere", "ID": "AccessChestAnywhere", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/257", "http://www.nexusmods.com/stardewvalley/mods/518" ], + "Defaults": { "NexusID": 257 }, + "AlternativeUrl": "https://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.1": { "Status": "AssumeBroken" } // broke in SDV 1.1 } @@ -56,7 +57,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "AdjustArtisanPrices", "ID": "1e36d4ca-c7ef-4dfb-9927-d27a6c3c8bdc", - "UpdateUrls": [ "http://community.playstarbound.com/resources/3532", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "ChucklefishID": 3532 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~0.1": { "Status": "AssumeBroken" } // broke in SMAPI 1.9 } @@ -64,7 +66,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Advanced Location Loader", "ID": "Entoarox.AdvancedLocationLoader", - "UpdateUrls": [ "http://community.playstarbound.com/resources/3619" ], + "Defaults": { "ChucklefishID": 3619 }, "Compatibility": { "~1.2.10": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -72,7 +74,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "AgingMod", "ID": "skn.AgingMod", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1129", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 1129 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -80,7 +83,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Almighty Tool", "ID": "AlmightyTool.dll", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/439" ], + "Defaults": { "NexusID": 439 }, "Compatibility": { "~1.1.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -95,7 +98,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Animal Sitter", "ID": "AnimalSitter.dll", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/581", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 581 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0.8": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -103,7 +107,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "A Tapper's Dream", "ID": "ddde5195-8f85-4061-90cc-0d4fd5459358", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/260", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 260 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -111,7 +116,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Better Sprinklers", "ID": "SPDSprinklersMod | Speeder.BetterSprinklers", // changed in 2.3 - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/41", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 41 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~2.3.1-pathoschild-update": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -119,7 +125,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Birthday Mail", "ID": "005e02dc-d900-425c-9c68-1ff55c5a295d", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/276", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 276 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.2.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -127,7 +134,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Build Endurance", "ID": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'BuildEndurance'}", // disambiguate from other Alpha_Omegasis mods - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/445", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 445 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -135,7 +143,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Build Health", "ID": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'BuildHealth'}", // disambiguate from other Alpha_Omegasis mods - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/446", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 446 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -143,7 +152,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Buy Cooking Recipes", "ID": "Denifia.BuyRecipes", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1126", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 1126 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -151,7 +161,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Buy Back Collectables", "ID": "BuyBackCollectables", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/507", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 507 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -159,7 +170,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Chest Label System", "ID": "SPDChestLabel", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/242", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 242 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.1 } @@ -167,7 +179,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Chest Pooling", "ID": "ChestPooling.dll", - "UpdateUrls": [ "http://community.playstarbound.com/threads/111988" ], + "Defaults": { "GitHubProject": "mralbobo/stardew-chest-pooling" }, "Compatibility": { "~1.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -175,7 +187,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Chests Anywhere", "ID": "ChestsAnywhere | Pathoschild.ChestsAnywhere", // changed in 1.9 - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/518" ], + "Defaults": { "NexusID": 518 }, "Compatibility": { "~1.9-beta": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -183,7 +195,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Choose Baby Gender", "ID": "ChooseBabyGender.dll", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/590", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 590 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -191,7 +204,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "CJB Automation", "ID": "CJBAutomation", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/211", "http://www.nexusmods.com/stardewvalley/mods/1063" ], + "Defaults": { "NexusID": 211 }, + "AlternativeUrl": "http://www.nexusmods.com/stardewvalley/mods/1063", "Compatibility": { "~1.4": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -199,7 +213,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "CJB Cheats Menu", "ID": "CJBCheatsMenu", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/4" ], + "Defaults": { "NexusID": 4 }, "Compatibility": { "~1.12": { "Status": "AssumeBroken" } // broke in SDV 1.1 } @@ -207,7 +221,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "CJB Item Spawner", "ID": "CJBItemSpawner", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/93" ], + "Defaults": { "NexusID": 93 }, "Compatibility": { "~1.5": { "Status": "AssumeBroken" } // broke in SDV 1.1 } @@ -215,7 +229,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "CJB Show Item Sell Price", "ID": "CJBShowItemSellPrice", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/93" ], + "Defaults": { "NexusID": 5 }, "Compatibility": { "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -223,7 +237,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Climates of Ferngill", "ID": "KoihimeNakamura.ClimatesOfFerngill", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/604", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 604 }, "Compatibility": { "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -231,7 +245,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Cold Weather Haley", "ID": "LordXamon.ColdWeatherHaleyPRO", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1169", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 1169 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -246,7 +261,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Combat with Farm Implements", "ID": "SPDFarmingImplementsInCombat", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/313", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 313 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -254,7 +270,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Configurable Shipping Dates", "ID": "ConfigurableShippingDates", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/675", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 675 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -262,7 +279,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Cooking Skill", "ID": "CookingSkill | spacechase0.CookingSkill", // changed in 1.0.4–6 - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/522" ], + "Defaults": { "NexusID": 522 }, "Compatibility": { "~1.0.6": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -270,7 +287,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "CrabNet", "ID": "CrabNet.dll", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/584", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 584 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -278,7 +296,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Customize Exterior", "ID": "CustomizeExterior", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1099" ], + "Defaults": { "NexusID": 1099 }, "Compatibility": { "~1.0.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -286,7 +304,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Customizable Traveling Cart Days", "ID": "TravelingCartYyeahdude", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/567", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 567 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -294,7 +313,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Daily News", "ID": "bashNinja.DailyNews", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1141", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 1141 }, "Compatibility": { "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -302,7 +321,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Dynamic Checklist", "ID": "gunnargolf.DynamicChecklist", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1145", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 1145 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -310,7 +330,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Dynamic Machines", "ID": "DynamicMachines", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/374", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 374 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -318,7 +339,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Empty Hands", "ID": "QuicksilverFox.EmptyHands", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1176", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 1176 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -326,7 +348,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Enemy Health Bars", "ID": "SPDHealthBar", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/193", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 193 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.7": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -334,7 +357,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Entoarox Framework", "ID": "eacdb74b-4080-4452-b16b-93773cda5cf9 | Entoarox.EntoaroxFramework", // changed in ??? - "UpdateUrls": [ "http://community.playstarbound.com/resources/4228" ], + "Defaults": { "ChucklefishID": 4228 }, "Compatibility": { "~1.7.9": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -342,7 +365,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Extended Fridge", "ID": "Mystra007ExtendedFridge", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/485", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 485 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -350,7 +374,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Extended Greenhouse", "ID": "ExtendedGreenhouse", - "UpdateUrls": [ "http://community.playstarbound.com/resources/4303", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "ChucklefishID": 4303 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -358,7 +383,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Fall 28 Snow Day", "ID": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Fall28 Snow Day'}", // disambiguate from other mods by Alpha_Omegasis - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/486", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 486 }, "Compatibility": { "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -366,7 +391,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Farm Automation: Barn Door Automation", "ID": "FarmAutomation.BarnDoorAutomation.dll", - "UpdateUrls": [ "http://community.playstarbound.com/threads/111931", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -374,7 +399,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Farm Automation: Item Collector", "ID": "FarmAutomation.ItemCollector.dll", - "UpdateUrls": [ "http://community.playstarbound.com/threads/111931", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -382,7 +407,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Farm Automation Unofficial: Item Collector", "ID": "Maddy99.FarmAutomation.ItemCollector", - "UpdateUrls": [ "http://community.playstarbound.com/threads/125172", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~0.5": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -390,7 +415,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Farm Expansion", "ID": "3888bdfd-73f6-4776-8bb7-8ad45aea1915 | AdvizeFarmExpansionMod-2-0 | AdvizeFarmExpansionMod-2-0-5", // changed in 2.0 and 2.0.5 - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/130", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 130 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~2.0.5": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -398,7 +424,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Farm Resource Generator", "ID": "FarmResourceGenerator.dll", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/647", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 647 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -406,7 +433,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Faster Run", "ID": "FasterRun.dll", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/733", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 733 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -414,7 +442,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "FlorenceMod", "ID": "FlorenceMod.dll", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/591", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 591 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -422,7 +451,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Forage at the Farm", "ID": "ForageAtTheFarm", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/673", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 673 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.5.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -430,7 +460,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Instant Geode", "ID": "InstantGeode", - "UpdateUrls": [ "http://community.playstarbound.com/threads/109038", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.12": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -438,7 +468,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Gate Opener", "ID": "GateOpener.dll | mralbobo.GateOpener", // changed in 1.1 - "UpdateUrls": [ "http://community.playstarbound.com/threads/111988" ], + "Defaults": { "GitHubProject": "mralbobo/stardew-gate-opener" }, "Compatibility": { "~1.0.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -446,7 +476,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "GenericShopExtender", "ID": "GenericShopExtender", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/814", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 814 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~0.1.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -454,7 +485,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Get Dressed", "ID": "GetDressed.dll | Advize.GetDressed", // changed in 3.3 - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/331" ], + "Defaults": { "NexusID": 331 }, "Compatibility": { "~3.3": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -462,7 +493,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Gift Taste Helper", "ID": "8008db57-fa67-4730-978e-34b37ef191d6", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/229" ], + "Defaults": { "NexusID": 229 }, "Compatibility": { "~2.3.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -470,7 +501,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Happy Animals", "ID": "HappyAnimals", - "UpdateUrls": [ "https://community.playstarbound.com/threads/126655", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -478,7 +509,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Happy Birthday", "ID": "{ID:'HappyBirthday', Author:'Alpha_Omegasis'}", // disambiguate from Oxyligen's fork - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/520", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 520 }, "Compatibility": { "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -486,7 +517,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Harvest With Scythe", "ID": "965169fd-e1ed-47d0-9f12-b104535fb4bc", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/236", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 236 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0.6": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -494,7 +526,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Hunger for Food", "ID": "HungerForFoodByTigerle", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/810", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 810 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~0.1.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -502,7 +535,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Improved Quality of Life", "ID": "Demiacle.ImprovedQualityOfLife", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1025", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 1025 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -510,7 +544,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Less Strict Over-Exertion (AntiExhaustion)", "ID": "BALANCEMOD_AntiExhaustion", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/637", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 637 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -518,7 +553,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Lookup Anything", "ID": "LookupAnything | Pathoschild.LookupAnything", // changed in 1.10.1 - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/541" ], + "Defaults": { "NexusID": 541 }, "Compatibility": { "~1.10.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -526,7 +561,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Loved Labels", "ID": "LovedLabels.dll", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/279" ], + "Defaults": { "NexusID": 279 }, "Compatibility": { "~2.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -534,7 +569,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Luck Skill", "ID": "LuckSkill | spacechase0.LuckSkill", // changed in 0.1.4 - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/521" ], + "Defaults": { "NexusID": 521 }, "Compatibility": { "~0.1.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -542,7 +577,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "MailOrderPigs", "ID": "MailOrderPigs.dll", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/632", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 632 }, "Compatibility": { "~1.0.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -550,7 +585,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Makeshift Multiplayer", "ID": "StardewValleyMP | spacechase0.StardewValleyMP", // changed in 0.3 - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/501", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 501 }, "Compatibility": { "~0.3.6": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -558,7 +593,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Message Box [API]? (ChatMod)", "ID": "Kithio:ChatMod", - "UpdateUrls": [ "http://community.playstarbound.com/resources/message-box-api.4296", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "ChucklefishID": 4296 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -573,7 +609,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "More Artifact Spots", "ID": "451", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/451", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 451 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -581,7 +618,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "More Pets", "ID": "821ce8f6-e629-41ad-9fde-03b54f68b0b6MOREPETS | Entoarox.MorePets", // changed in 1.3 - "UpdateUrls": [ "http://community.playstarbound.com/resources/4288" ], + "Defaults": { "ChucklefishID": 4288 }, "Compatibility": { "~1.3.2": { "Status": "AssumeBroken" } // overhauled for SMAPI 1.11+ compatibility } @@ -589,7 +626,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "More Rain", "ID": "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'More_Rain'}", // disambiguate from other mods by Alpha_Omegasis - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/441", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 441 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -597,7 +635,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Multiple Sprites and Portraits On Rotation (File Loading)", "ID": "FileLoading", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1094", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 1094 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.12": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -605,7 +644,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Museum Rearranger", "ID": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Museum Rearranger'}", // disambiguate from other mods by Alpha_Omegasis - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/428", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 428 }, "Compatibility": { "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -613,15 +652,16 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "New Machines", "ID": "F70D4FAB-0AB2-4B78-9F1B-AF2CA2236A59", - "UpdateUrls": [ "http://community.playstarbound.com/resources/3683", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "ChucklefishID": 3683 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~4.2.1343": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } }, { "Name": "Night Owl", + "Defaults": { "NexusID": 433 }, "ID": "{ID:'SaveAnywhere', Name:'Stardew_NightOwl'}", // disambiguate from Save Anywhere - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/433", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], "Compatibility": { "~2.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -636,7 +676,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "NoSoilDecay", "ID": "289dee03-5f38-4d8e-8ffc-e440198e8610", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/237", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 237 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~0.5": { "Status": "AssumeBroken" } // broke in SDV 1.2, and uses Assembly.GetExecutingAssembly().Location } @@ -644,7 +685,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "NPC Map Locations", "ID": "NPCMapLocationsMod", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/239" ], + "Defaults": { "NexusID": 239 }, "Compatibility": { "1.42~1.43": { "Status": "AssumeBroken", "ReasonPhrase": "this version has an update check error which crashes the game." } } @@ -652,7 +693,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "NPC Speak", "ID": "NpcEcho.dll", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/694", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 694 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -660,7 +702,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "OmniFarm", "ID": "BlueMod_OmniFarm", - "UpdateUrls": [ "http://community.playstarbound.com/threads/127299", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "GitHubProject": "lambui/StardewValleyMod_OmniFarm" }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~2.0.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -668,7 +711,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Part of the Community", "ID": "SB_PotC", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/923" ], + "Defaults": { "NexusID": 923 }, "Compatibility": { "~1.0.8": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -676,7 +719,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "PelicanFiber", "ID": "PelicanFiber.dll", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/631", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 631 }, "Compatibility": { "~3.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -684,7 +727,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "PelicanTTS", "ID": "Platonymous.PelicanTTS", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1079", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 1079 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.6": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -692,7 +736,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Persival's BundleMod", "ID": "BundleMod.dll", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/438", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 438 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.1 } @@ -700,7 +745,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Point-and-Plant", "ID": "PointAndPlant.dll", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/572", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 572 }, "Compatibility": { "~1.0.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -708,7 +753,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "PrairieKingMadeEasy", "ID": "PrairieKingMadeEasy.dll", - "UpdateUrls": [ "http://community.playstarbound.com/resources/3594", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "ChucklefishID": 3594 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -716,7 +762,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "RainRandomizer", "ID": "RainRandomizer.dll", - "UpdateUrls": [ "http://community.playstarbound.com/threads/111526", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -724,7 +770,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "RelationshipsEnhanced", "ID": "relationshipsenhanced", - "UpdateUrls": [ "http://community.playstarbound.com/resources/4435", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "ChucklefishID": 4435 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -732,7 +779,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "RelationShipStatus", "ID": "relationshipstatus", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/751", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 751 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0.5": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -740,7 +788,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Replanter", "ID": "Replanter.dll", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/589", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 589 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -748,7 +797,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Reusable Wallpapers and Floors (Wallpaper Retain)", "ID": "dae1b553-2e39-43e7-8400-c7c5c836134b", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/356", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 356 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.5": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -756,7 +806,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Rush Orders", "ID": "RushOrders | spacechase0.RushOrders", // changed in 1.1 - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/605" ], + "Defaults": { "NexusID": 605 }, "Compatibility": { "~1.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -764,7 +814,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Save Anywhere", "ID": "{ID:'SaveAnywhere', Name:'Save Anywhere'}", // disambiguate from Night Owl - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/444", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 444 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~2.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -772,7 +823,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Seasonal Immersion", "ID": "EntoaroxSeasonalHouse | EntoaroxSeasonalBuildings | EntoaroxSeasonalImmersion | Entoarox.SeasonalImmersion", // changed in 1.1, 1.6 or earlier, and 1.7 - "UpdateUrls": [ "http://community.playstarbound.com/resources/4262", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "ChucklefishID": 4262 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.8.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -780,7 +832,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Send Items", "ID": "Denifia.SendItems", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1087", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 1087 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -788,7 +841,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Shed Notifications (BuildingsNotifications)", "ID": "TheCroak.BuildingsNotifications", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/620", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 620 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~0.4.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -796,7 +850,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Shenandoah Project", "ID": "Shenandoah Project", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/756", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 756 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -804,7 +859,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Shipment Tracker", "ID": "7e474181-e1a0-40f9-9c11-d08a3dcefaf3", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/321", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 321 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -812,7 +868,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Shop Expander", "ID": "821ce8f6-e629-41ad-9fde-03b54f68b0b6 | EntoaroxShopExpander | Entoarox.ShopExpander", // changed in 1.5 and 1.5.2 - "UpdateUrls": [ "http://community.playstarbound.com/resources/4381", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "ChucklefishID": 4381 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.5.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -820,7 +877,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Showcase Mod", "ID": "Igorious.Showcase", - "UpdateUrls": [ "http://community.playstarbound.com/resources/4487", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "ChucklefishID": 4487 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~0.9": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -828,7 +886,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Simple Sprinklers", "ID": "SimpleSprinkler.dll", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/76", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 76 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.4": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -836,7 +895,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Siv's Marriage Mod", "ID": "6266959802", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/366", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 366 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.2.2": { "Status": "AssumeBroken" } // broke in SMAPI 1.9 (has multiple Mod instances) } @@ -844,7 +904,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Skill Prestige", "ID": "6b843e60-c8fc-4a25-a67b-4a38ac8dcf9b", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/569", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 569 }, "Compatibility": { "~1.0.9": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -852,7 +912,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Skill Prestige: Cooking Adapter", "ID": "20d6b8a3-b6e7-460b-a6e4-07c2b0cb6c63", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/569", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 569 }, "Compatibility": { "~1.0.9": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -860,7 +920,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Slower Fence Decay", "ID": "SPDSlowFenceDecay", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/252", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 252 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~0.5.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -868,7 +929,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Smart Mod", "ID": "KuroBear.SmartMod", - "UpdateUrls": [ "http://community.playstarbound.com/threads/108104", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~2.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -876,7 +937,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Solar Eclipse Event", "ID": "KoihimeNakamura.SolarEclipseEvent", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/897", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 897 }, "Compatibility": { "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -884,7 +945,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Sprinkles", "ID": "Platonymous.Sprinkles", - "UpdateUrls": [ "http://community.playstarbound.com/resources/4592", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "ChucklefishID": 4592 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -892,7 +954,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Sprint and Dash", "ID": "SPDSprintAndDash", - "UpdateUrls": [ "http://community.playstarbound.com/resources/3531", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "ChucklefishID": 3531 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -900,7 +963,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Sprint and Dash Redux", "ID": "SPDSprintAndDash", - "UpdateUrls": [ "http://community.playstarbound.com/resources/4201" ], + "Defaults": { "ChucklefishID": 4201 }, "Compatibility": { "~1.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -908,7 +971,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Sprinting Mod", "ID": "a10d3097-b073-4185-98ba-76b586cba00c", - "UpdateUrls": [ "http://community.playstarbound.com/threads/108313", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "GitHubProject": "oliverpl/SprintingMod" }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~2.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -916,7 +980,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "StackSplitX", "ID": "StackSplitX.dll", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/798" ], + "Defaults": { "NexusID": 798 }, "Compatibility": { "~1.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -924,7 +988,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "StaminaRegen", "ID": "StaminaRegen.dll", - "UpdateUrls": [ "http://community.playstarbound.com/threads/111526", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -932,7 +996,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Stardew Auto Backup", "ID": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'Stardew_Save_Backup'}", // disambiguate from other Alpha_Omegasis mods - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/435", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 435 }, "Compatibility": { "~1.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -940,7 +1004,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Stardew Notification", "ID": "stardewnotification", - "UpdateUrls": [ "http://community.playstarbound.com/threads/127979", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "GitHubProject": "monopandora/StardewNotification" }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.7": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -948,7 +1013,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Stardew Symphony", "ID": "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'Stardew_Symphony'}", // disambiguate other mods by Alpha_Omegasis - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/425", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 425 }, "Compatibility": { "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -963,7 +1028,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "StashItemsToChest", "ID": "BlueMod_StashItemsToChest", - "UpdateUrls": [ "http://community.playstarbound.com/threads/126906", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "GitHubProject": "lambui/StardewValleyMod_StashItemsToChest" }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -971,7 +1037,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Stone Bridge Over Pond (PondWithBridge)", "ID": "PondWithBridge.dll", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/316", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 316 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -979,7 +1046,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Super Greenhouse Warp Modifier", "ID": "SuperGreenhouse", - "UpdateUrls": [ "http://community.playstarbound.com/resources/4334", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "ChucklefishID": 4334 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -987,7 +1055,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Tainted Cellar", "ID": "TaintedCellar.dll", - "UpdateUrls": [ "http://community.playstarbound.com/threads/115735", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.1 or 1.11 } @@ -995,7 +1063,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Teleporter", "ID": "Teleporter", - "UpdateUrls": [ "http://community.playstarbound.com/resources/4374", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "ChucklefishID": 4374 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -1003,7 +1072,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Three-heart Dance Partner", "ID": "ThreeHeartDancePartner", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/500", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 500 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -1011,7 +1081,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "TimeSpeed", "ID": "TimeSpeed.dll | {ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'TimeSpeed'} | {ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'TimeSpeed Mod (unofficial)'} | community.TimeSpeed", // changed in 2.0.3 and 2.1; disambiguate other mods by Alpha_Omegasis - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/169" ], + "Defaults": { "NexusID": 169 }, "Compatibility": { "~2.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -1019,7 +1089,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "UiModSuite", "ID": "Demiacle.UiModSuite", - "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1023", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "NexusID": 1023 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -1027,7 +1098,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "WakeUp", "ID": "WakeUp.dll", - "UpdateUrls": [ "http://community.playstarbound.com/threads/111526", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -1035,7 +1106,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Wallpaper Fix", "ID": "WallpaperFix.dll", - "UpdateUrls": [ "http://community.playstarbound.com/resources/4211", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "ChucklefishID": 4211 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -1043,7 +1115,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Weather Controller", "ID": "WeatherController.dll", - "UpdateUrls": [ "http://community.playstarbound.com/threads/111526", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -1051,7 +1123,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Wonderful Farm Life", "ID": "WonderfulFarmLife.dll", - "UpdateUrls": [ "http://community.playstarbound.com/threads/115384", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.1 or 1.11 } @@ -1066,7 +1138,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Xnb Loader", "ID": "Entoarox.XnbLoader", - "UpdateUrls": [ "http://community.playstarbound.com/resources/4506", "http://stardewvalleywiki.com/Modding:SMAPI_2.0" ], + "Defaults": { "ChucklefishID": 4506 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0.6": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -1074,7 +1147,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "zDailyIncrease", "ID": "zdailyincrease", - "UpdateUrls": [ "http://community.playstarbound.com/resources/4247" ], + "Defaults": { "ChucklefishID": 4247 }, "Compatibility": { "~1.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -1082,7 +1155,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Zoom Out Extreme", "ID": "ZoomMod", - "UpdateUrls": [ "http://community.playstarbound.com/threads/115028" ], + "AlternativeUrl": "http://community.playstarbound.com/threads/115028", "Compatibility": { "~0.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -1090,7 +1163,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Zoryn's Better RNG", "ID": "76b6d1e1-f7ba-4d72-8c32-5a1e6d2716f6 | Zoryn.BetterRNG", // changed in 1.6 - "UpdateUrls": [ "https://github.com/Zoryn4163/SMAPI-Mods/releases" ], + "Defaults": { "GitHubProject": "Zoryn4163/SMAPI-Mods" }, "Compatibility": { "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -1098,7 +1171,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Zoryn's Calendar Anywhere", "ID": "a41c01cd-0437-43eb-944f-78cb5a53002a | Zoryn.CalendarAnywhere", // changed in 1.6 - "UpdateUrls": [ "https://github.com/Zoryn4163/SMAPI-Mods/releases" ], + "Defaults": { "GitHubProject": "Zoryn4163/SMAPI-Mods" }, "Compatibility": { "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -1106,7 +1179,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Zoryn's Health Bars", "ID": "HealthBars.dll | Zoryn.HealthBars", // changed in 1.6 - "UpdateUrls": [ "https://github.com/Zoryn4163/SMAPI-Mods/releases" ], + "Defaults": { "GitHubProject": "Zoryn4163/SMAPI-Mods" }, "Compatibility": { "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -1114,7 +1187,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Zoryn's Junimo Deposit Anywhere", "ID": "f93a4fe8-cade-4146-9335-b5f82fbbf7bc | Zoryn.JunimoDepositAnywhere", // changed in 1.6 - "UpdateUrls": [ "https://github.com/Zoryn4163/SMAPI-Mods/releases" ], + "Defaults": { "GitHubProject": "Zoryn4163/SMAPI-Mods" }, "Compatibility": { "~1.7": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -1122,7 +1195,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Zoryn's Movement Mod", "ID": "8a632929-8335-484f-87dd-c29d2ba3215d | Zoryn.MovementModifier", // changed in 1.6 - "UpdateUrls": [ "https://github.com/Zoryn4163/SMAPI-Mods/releases" ], + "Defaults": { "GitHubProject": "Zoryn4163/SMAPI-Mods" }, "Compatibility": { "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -1130,7 +1203,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Zoryn's Regen Mod", "ID": "dfac4383-1b6b-4f33-ae4e-37fc23e5252e | Zoryn.RegenMod", // changed in 1.6 - "UpdateUrls": [ "https://github.com/Zoryn4163/SMAPI-Mods/releases" ], + "Defaults": { "GitHubProject": "Zoryn4163/SMAPI-Mods" }, "Compatibility": { "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.2 } diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 8863590b..5bf46ac6 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -91,6 +91,7 @@ Properties\GlobalAssemblyInfo.cs + From 93fb34223cb5bbdf7d3ee4eec44192d4638b8d6b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 Sep 2017 02:38:29 -0400 Subject: [PATCH 105/186] update release notes (#360, #361) --- release-notes.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/release-notes.md b/release-notes.md index 41d946c7..a1473313 100644 --- a/release-notes.md +++ b/release-notes.md @@ -6,7 +6,8 @@ For players: * The console is now simpler and easier to read. * The console now adjusts its colors when you have a light terminal background. * SMAPI now detects mods which may impact game stability and shows a warning in the console. -* SMAPI now alerts you in the console when one of your mods has a new version. +* SMAPI now alerts you in the console when one of your mods has a new version. + _That includes most existing mods, even if they haven't updated to use the new update-check API yet._ * Renamed installer folder from `SMAPI 2.0` to `SMAPI 2.0 installer` to avoid confusion. * Updated compatibility list. * Fixed update check errors on Linux/Mac. @@ -33,6 +34,8 @@ For power users: For SMAPI developers: * SMAPI has been significantly refactored under the hood to support changes in 2.0 and upcoming releases. +* Overhauled `StardewModdingAPI.config.json` format to support injecting mod data. +* Removed SMAPI 1._x_ compatibility code. ## 1.15.4 For players: From ce9be43db3d19fd0264890be38e2b6535d557ec7 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 Sep 2017 11:28:08 -0400 Subject: [PATCH 106/186] remove name from data record (#361) --- .../Framework/Models/ModDataRecord.cs | 3 - .../StardewModdingAPI.config.json | 274 +++++++++--------- 2 files changed, 137 insertions(+), 140 deletions(-) diff --git a/src/StardewModdingAPI/Framework/Models/ModDataRecord.cs b/src/StardewModdingAPI/Framework/Models/ModDataRecord.cs index 9f19d5f0..a2397fa8 100644 --- a/src/StardewModdingAPI/Framework/Models/ModDataRecord.cs +++ b/src/StardewModdingAPI/Framework/Models/ModDataRecord.cs @@ -14,9 +14,6 @@ namespace StardewModdingAPI.Framework.Models [JsonConverter(typeof(SFieldConverter))] public ModDataID ID { get; set; } - /// The mod name. - public string Name { get; set; } - /// Default values for support fields to inject into the manifest. public ModDataDefaults Defaults { get; set; } diff --git a/src/StardewModdingAPI/StardewModdingAPI.config.json b/src/StardewModdingAPI/StardewModdingAPI.config.json index d2d28625..de7efcf8 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.config.json +++ b/src/StardewModdingAPI/StardewModdingAPI.config.json @@ -46,7 +46,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha */ "ModData": [ { - "Name": "AccessChestAnywhere", + // AccessChestAnywhere "ID": "AccessChestAnywhere", "Defaults": { "NexusID": 257 }, "AlternativeUrl": "https://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -55,7 +55,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "AdjustArtisanPrices", + // AdjustArtisanPrices "ID": "1e36d4ca-c7ef-4dfb-9927-d27a6c3c8bdc", "Defaults": { "ChucklefishID": 3532 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -64,7 +64,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Advanced Location Loader", + // Advanced Location Loader "ID": "Entoarox.AdvancedLocationLoader", "Defaults": { "ChucklefishID": 3619 }, "Compatibility": { @@ -72,7 +72,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "AgingMod", + // AgingMod "ID": "skn.AgingMod", "Defaults": { "NexusID": 1129 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -81,7 +81,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Almighty Tool", + // Almighty Tool "ID": "AlmightyTool.dll", "Defaults": { "NexusID": 439 }, "Compatibility": { @@ -89,14 +89,14 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Animal Mood Fix", + // Animal Mood Fix "ID": "GPeters-AnimalMoodFix", "Compatibility": { "~": { "Status": "Obsolete", "ReasonPhrase": "the animal mood bugs were fixed in Stardew Valley 1.2." } } }, { - "Name": "Animal Sitter", + // Animal Sitter "ID": "AnimalSitter.dll", "Defaults": { "NexusID": 581 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -105,7 +105,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "A Tapper's Dream", + // A Tapper's Dream "ID": "ddde5195-8f85-4061-90cc-0d4fd5459358", "Defaults": { "NexusID": 260 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -114,7 +114,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Better Sprinklers", + // Better Sprinklers "ID": "SPDSprinklersMod | Speeder.BetterSprinklers", // changed in 2.3 "Defaults": { "NexusID": 41 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -123,7 +123,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Birthday Mail", + // Birthday Mail "ID": "005e02dc-d900-425c-9c68-1ff55c5a295d", "Defaults": { "NexusID": 276 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -132,7 +132,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Build Endurance", + // Build Endurance "ID": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'BuildEndurance'}", // disambiguate from other Alpha_Omegasis mods "Defaults": { "NexusID": 445 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -141,7 +141,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Build Health", + // Build Health "ID": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'BuildHealth'}", // disambiguate from other Alpha_Omegasis mods "Defaults": { "NexusID": 446 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -150,7 +150,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Buy Cooking Recipes", + // Buy Cooking Recipes "ID": "Denifia.BuyRecipes", "Defaults": { "NexusID": 1126 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -159,7 +159,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Buy Back Collectables", + // Buy Back Collectables "ID": "BuyBackCollectables", "Defaults": { "NexusID": 507 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -168,7 +168,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Chest Label System", + // Chest Label System "ID": "SPDChestLabel", "Defaults": { "NexusID": 242 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -177,7 +177,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Chest Pooling", + // Chest Pooling "ID": "ChestPooling.dll", "Defaults": { "GitHubProject": "mralbobo/stardew-chest-pooling" }, "Compatibility": { @@ -185,7 +185,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Chests Anywhere", + // Chests Anywhere "ID": "ChestsAnywhere | Pathoschild.ChestsAnywhere", // changed in 1.9 "Defaults": { "NexusID": 518 }, "Compatibility": { @@ -193,7 +193,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Choose Baby Gender", + // Choose Baby Gender "ID": "ChooseBabyGender.dll", "Defaults": { "NexusID": 590 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -202,7 +202,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "CJB Automation", + // CJB Automation "ID": "CJBAutomation", "Defaults": { "NexusID": 211 }, "AlternativeUrl": "http://www.nexusmods.com/stardewvalley/mods/1063", @@ -211,7 +211,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "CJB Cheats Menu", + // CJB Cheats Menu "ID": "CJBCheatsMenu", "Defaults": { "NexusID": 4 }, "Compatibility": { @@ -219,7 +219,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "CJB Item Spawner", + // CJB Item Spawner "ID": "CJBItemSpawner", "Defaults": { "NexusID": 93 }, "Compatibility": { @@ -227,7 +227,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "CJB Show Item Sell Price", + // CJB Show Item Sell Price "ID": "CJBShowItemSellPrice", "Defaults": { "NexusID": 5 }, "Compatibility": { @@ -235,7 +235,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Climates of Ferngill", + // Climates of Ferngill "ID": "KoihimeNakamura.ClimatesOfFerngill", "Defaults": { "NexusID": 604 }, "Compatibility": { @@ -243,7 +243,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Cold Weather Haley", + // Cold Weather Haley "ID": "LordXamon.ColdWeatherHaleyPRO", "Defaults": { "NexusID": 1169 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -252,14 +252,14 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Colored Chests", + // Colored Chests "ID": "4befde5c-731c-4853-8e4b-c5cdf946805f", "Compatibility": { "~": { "Status": "Obsolete", "ReasonPhrase": "colored chests were added in Stardew Valley 1.1." } } }, { - "Name": "Combat with Farm Implements", + // Combat with Farm Implements "ID": "SPDFarmingImplementsInCombat", "Defaults": { "NexusID": 313 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -268,7 +268,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Configurable Shipping Dates", + // Configurable Shipping Dates "ID": "ConfigurableShippingDates", "Defaults": { "NexusID": 675 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -277,7 +277,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Cooking Skill", + // Cooking Skill "ID": "CookingSkill | spacechase0.CookingSkill", // changed in 1.0.4–6 "Defaults": { "NexusID": 522 }, "Compatibility": { @@ -285,7 +285,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "CrabNet", + // CrabNet "ID": "CrabNet.dll", "Defaults": { "NexusID": 584 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -294,7 +294,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Customize Exterior", + // Customize Exterior "ID": "CustomizeExterior", "Defaults": { "NexusID": 1099 }, "Compatibility": { @@ -302,7 +302,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Customizable Traveling Cart Days", + // Customizable Traveling Cart Days "ID": "TravelingCartYyeahdude", "Defaults": { "NexusID": 567 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -311,7 +311,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Daily News", + // Daily News "ID": "bashNinja.DailyNews", "Defaults": { "NexusID": 1141 }, "Compatibility": { @@ -319,7 +319,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Dynamic Checklist", + // Dynamic Checklist "ID": "gunnargolf.DynamicChecklist", "Defaults": { "NexusID": 1145 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -328,7 +328,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Dynamic Machines", + // Dynamic Machines "ID": "DynamicMachines", "Defaults": { "NexusID": 374 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -337,7 +337,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Empty Hands", + // Empty Hands "ID": "QuicksilverFox.EmptyHands", "Defaults": { "NexusID": 1176 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -346,7 +346,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Enemy Health Bars", + // Enemy Health Bars "ID": "SPDHealthBar", "Defaults": { "NexusID": 193 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -355,7 +355,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Entoarox Framework", + // Entoarox Framework "ID": "eacdb74b-4080-4452-b16b-93773cda5cf9 | Entoarox.EntoaroxFramework", // changed in ??? "Defaults": { "ChucklefishID": 4228 }, "Compatibility": { @@ -363,7 +363,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Extended Fridge", + // Extended Fridge "ID": "Mystra007ExtendedFridge", "Defaults": { "NexusID": 485 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -372,7 +372,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Extended Greenhouse", + // Extended Greenhouse "ID": "ExtendedGreenhouse", "Defaults": { "ChucklefishID": 4303 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -381,7 +381,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Fall 28 Snow Day", + // Fall 28 Snow Day "ID": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Fall28 Snow Day'}", // disambiguate from other mods by Alpha_Omegasis "Defaults": { "NexusID": 486 }, "Compatibility": { @@ -389,7 +389,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Farm Automation: Barn Door Automation", + // Farm Automation: Barn Door Automation "ID": "FarmAutomation.BarnDoorAutomation.dll", "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { @@ -397,7 +397,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Farm Automation: Item Collector", + // Farm Automation: Item Collector "ID": "FarmAutomation.ItemCollector.dll", "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { @@ -405,7 +405,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Farm Automation Unofficial: Item Collector", + // Farm Automation Unofficial: Item Collector "ID": "Maddy99.FarmAutomation.ItemCollector", "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { @@ -413,7 +413,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Farm Expansion", + // Farm Expansion "ID": "3888bdfd-73f6-4776-8bb7-8ad45aea1915 | AdvizeFarmExpansionMod-2-0 | AdvizeFarmExpansionMod-2-0-5", // changed in 2.0 and 2.0.5 "Defaults": { "NexusID": 130 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -422,7 +422,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Farm Resource Generator", + // Farm Resource Generator "ID": "FarmResourceGenerator.dll", "Defaults": { "NexusID": 647 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -431,7 +431,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Faster Run", + // Faster Run "ID": "FasterRun.dll", "Defaults": { "NexusID": 733 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -440,7 +440,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "FlorenceMod", + // FlorenceMod "ID": "FlorenceMod.dll", "Defaults": { "NexusID": 591 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -449,7 +449,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Forage at the Farm", + // Forage at the Farm "ID": "ForageAtTheFarm", "Defaults": { "NexusID": 673 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -458,7 +458,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Instant Geode", + // Instant Geode "ID": "InstantGeode", "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { @@ -466,7 +466,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Gate Opener", + // Gate Opener "ID": "GateOpener.dll | mralbobo.GateOpener", // changed in 1.1 "Defaults": { "GitHubProject": "mralbobo/stardew-gate-opener" }, "Compatibility": { @@ -474,7 +474,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "GenericShopExtender", + // GenericShopExtender "ID": "GenericShopExtender", "Defaults": { "NexusID": 814 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -483,7 +483,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Get Dressed", + // Get Dressed "ID": "GetDressed.dll | Advize.GetDressed", // changed in 3.3 "Defaults": { "NexusID": 331 }, "Compatibility": { @@ -491,7 +491,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Gift Taste Helper", + // Gift Taste Helper "ID": "8008db57-fa67-4730-978e-34b37ef191d6", "Defaults": { "NexusID": 229 }, "Compatibility": { @@ -499,7 +499,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Happy Animals", + // Happy Animals "ID": "HappyAnimals", "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { @@ -507,7 +507,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Happy Birthday", + // Happy Birthday "ID": "{ID:'HappyBirthday', Author:'Alpha_Omegasis'}", // disambiguate from Oxyligen's fork "Defaults": { "NexusID": 520 }, "Compatibility": { @@ -515,7 +515,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Harvest With Scythe", + // Harvest With Scythe "ID": "965169fd-e1ed-47d0-9f12-b104535fb4bc", "Defaults": { "NexusID": 236 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -524,7 +524,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Hunger for Food", + // Hunger for Food "ID": "HungerForFoodByTigerle", "Defaults": { "NexusID": 810 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -533,7 +533,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Improved Quality of Life", + // Improved Quality of Life "ID": "Demiacle.ImprovedQualityOfLife", "Defaults": { "NexusID": 1025 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -542,7 +542,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Less Strict Over-Exertion (AntiExhaustion)", + // Less Strict Over-Exertion (AntiExhaustion) "ID": "BALANCEMOD_AntiExhaustion", "Defaults": { "NexusID": 637 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -551,7 +551,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Lookup Anything", + // Lookup Anything "ID": "LookupAnything | Pathoschild.LookupAnything", // changed in 1.10.1 "Defaults": { "NexusID": 541 }, "Compatibility": { @@ -559,7 +559,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Loved Labels", + // Loved Labels "ID": "LovedLabels.dll", "Defaults": { "NexusID": 279 }, "Compatibility": { @@ -567,7 +567,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Luck Skill", + // Luck Skill "ID": "LuckSkill | spacechase0.LuckSkill", // changed in 0.1.4 "Defaults": { "NexusID": 521 }, "Compatibility": { @@ -575,7 +575,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "MailOrderPigs", + // MailOrderPigs "ID": "MailOrderPigs.dll", "Defaults": { "NexusID": 632 }, "Compatibility": { @@ -583,7 +583,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Makeshift Multiplayer", + // Makeshift Multiplayer "ID": "StardewValleyMP | spacechase0.StardewValleyMP", // changed in 0.3 "Defaults": { "NexusID": 501 }, "Compatibility": { @@ -591,7 +591,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Message Box [API]? (ChatMod)", + // Message Box [API]? (ChatMod) "ID": "Kithio:ChatMod", "Defaults": { "ChucklefishID": 4296 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -600,14 +600,14 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Modder Serialization Utility", + // Modder Serialization Utility "ID": "SerializerUtils-0-1", "Compatibility": { "~": { "Status": "Obsolete", "ReasonPhrase": "it's no longer maintained or used." } } }, { - "Name": "More Artifact Spots", + // More Artifact Spots "ID": "451", "Defaults": { "NexusID": 451 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -616,7 +616,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "More Pets", + // More Pets "ID": "821ce8f6-e629-41ad-9fde-03b54f68b0b6MOREPETS | Entoarox.MorePets", // changed in 1.3 "Defaults": { "ChucklefishID": 4288 }, "Compatibility": { @@ -624,7 +624,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "More Rain", + // More Rain "ID": "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'More_Rain'}", // disambiguate from other mods by Alpha_Omegasis "Defaults": { "NexusID": 441 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -633,7 +633,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Multiple Sprites and Portraits On Rotation (File Loading)", + // Multiple Sprites and Portraits On Rotation (File Loading) "ID": "FileLoading", "Defaults": { "NexusID": 1094 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -642,7 +642,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Museum Rearranger", + // Museum Rearranger "ID": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Museum Rearranger'}", // disambiguate from other mods by Alpha_Omegasis "Defaults": { "NexusID": 428 }, "Compatibility": { @@ -650,7 +650,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "New Machines", + // New Machines "ID": "F70D4FAB-0AB2-4B78-9F1B-AF2CA2236A59", "Defaults": { "ChucklefishID": 3683 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -659,7 +659,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Night Owl", + // Night Owl "Defaults": { "NexusID": 433 }, "ID": "{ID:'SaveAnywhere', Name:'Stardew_NightOwl'}", // disambiguate from Save Anywhere "Compatibility": { @@ -667,14 +667,14 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "No Debug Mode", + // No Debug Mode "ID": "NoDebugMode", "Compatibility": { "~": { "Status": "Obsolete", "ReasonPhrase": "debug mode was removed in SMAPI 1.0." } } }, { - "Name": "NoSoilDecay", + // NoSoilDecay "ID": "289dee03-5f38-4d8e-8ffc-e440198e8610", "Defaults": { "NexusID": 237 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -683,7 +683,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "NPC Map Locations", + // NPC Map Locations "ID": "NPCMapLocationsMod", "Defaults": { "NexusID": 239 }, "Compatibility": { @@ -691,7 +691,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "NPC Speak", + // NPC Speak "ID": "NpcEcho.dll", "Defaults": { "NexusID": 694 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -700,7 +700,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "OmniFarm", + // OmniFarm "ID": "BlueMod_OmniFarm", "Defaults": { "GitHubProject": "lambui/StardewValleyMod_OmniFarm" }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -709,7 +709,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Part of the Community", + // Part of the Community "ID": "SB_PotC", "Defaults": { "NexusID": 923 }, "Compatibility": { @@ -717,7 +717,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "PelicanFiber", + // PelicanFiber "ID": "PelicanFiber.dll", "Defaults": { "NexusID": 631 }, "Compatibility": { @@ -725,7 +725,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "PelicanTTS", + // PelicanTTS "ID": "Platonymous.PelicanTTS", "Defaults": { "NexusID": 1079 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -734,7 +734,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Persival's BundleMod", + // Persival's BundleMod "ID": "BundleMod.dll", "Defaults": { "NexusID": 438 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -743,7 +743,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Point-and-Plant", + // Point-and-Plant "ID": "PointAndPlant.dll", "Defaults": { "NexusID": 572 }, "Compatibility": { @@ -751,7 +751,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "PrairieKingMadeEasy", + // PrairieKingMadeEasy "ID": "PrairieKingMadeEasy.dll", "Defaults": { "ChucklefishID": 3594 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -760,7 +760,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "RainRandomizer", + // RainRandomizer "ID": "RainRandomizer.dll", "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { @@ -768,7 +768,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "RelationshipsEnhanced", + // RelationshipsEnhanced "ID": "relationshipsenhanced", "Defaults": { "ChucklefishID": 4435 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -777,7 +777,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "RelationShipStatus", + // RelationShipStatus "ID": "relationshipstatus", "Defaults": { "NexusID": 751 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -786,7 +786,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Replanter", + // Replanter "ID": "Replanter.dll", "Defaults": { "NexusID": 589 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -795,7 +795,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Reusable Wallpapers and Floors (Wallpaper Retain)", + // Reusable Wallpapers and Floors (Wallpaper Retain) "ID": "dae1b553-2e39-43e7-8400-c7c5c836134b", "Defaults": { "NexusID": 356 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -804,7 +804,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Rush Orders", + // Rush Orders "ID": "RushOrders | spacechase0.RushOrders", // changed in 1.1 "Defaults": { "NexusID": 605 }, "Compatibility": { @@ -812,7 +812,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Save Anywhere", + // Save Anywhere "ID": "{ID:'SaveAnywhere', Name:'Save Anywhere'}", // disambiguate from Night Owl "Defaults": { "NexusID": 444 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -821,7 +821,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Seasonal Immersion", + // Seasonal Immersion "ID": "EntoaroxSeasonalHouse | EntoaroxSeasonalBuildings | EntoaroxSeasonalImmersion | Entoarox.SeasonalImmersion", // changed in 1.1, 1.6 or earlier, and 1.7 "Defaults": { "ChucklefishID": 4262 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -830,7 +830,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Send Items", + // Send Items "ID": "Denifia.SendItems", "Defaults": { "NexusID": 1087 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -839,7 +839,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Shed Notifications (BuildingsNotifications)", + // Shed Notifications (BuildingsNotifications) "ID": "TheCroak.BuildingsNotifications", "Defaults": { "NexusID": 620 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -848,7 +848,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Shenandoah Project", + // Shenandoah Project "ID": "Shenandoah Project", "Defaults": { "NexusID": 756 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -857,7 +857,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Shipment Tracker", + // Shipment Tracker "ID": "7e474181-e1a0-40f9-9c11-d08a3dcefaf3", "Defaults": { "NexusID": 321 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -866,7 +866,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Shop Expander", + // Shop Expander "ID": "821ce8f6-e629-41ad-9fde-03b54f68b0b6 | EntoaroxShopExpander | Entoarox.ShopExpander", // changed in 1.5 and 1.5.2 "Defaults": { "ChucklefishID": 4381 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -875,7 +875,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Showcase Mod", + // Showcase Mod "ID": "Igorious.Showcase", "Defaults": { "ChucklefishID": 4487 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -884,7 +884,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Simple Sprinklers", + // Simple Sprinklers "ID": "SimpleSprinkler.dll", "Defaults": { "NexusID": 76 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -893,7 +893,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Siv's Marriage Mod", + // Siv's Marriage Mod "ID": "6266959802", "Defaults": { "NexusID": 366 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -902,7 +902,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Skill Prestige", + // Skill Prestige "ID": "6b843e60-c8fc-4a25-a67b-4a38ac8dcf9b", "Defaults": { "NexusID": 569 }, "Compatibility": { @@ -910,7 +910,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Skill Prestige: Cooking Adapter", + // Skill Prestige: Cooking Adapter "ID": "20d6b8a3-b6e7-460b-a6e4-07c2b0cb6c63", "Defaults": { "NexusID": 569 }, "Compatibility": { @@ -918,7 +918,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Slower Fence Decay", + // Slower Fence Decay "ID": "SPDSlowFenceDecay", "Defaults": { "NexusID": 252 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -927,7 +927,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Smart Mod", + // Smart Mod "ID": "KuroBear.SmartMod", "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { @@ -935,7 +935,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Solar Eclipse Event", + // Solar Eclipse Event "ID": "KoihimeNakamura.SolarEclipseEvent", "Defaults": { "NexusID": 897 }, "Compatibility": { @@ -943,7 +943,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Sprinkles", + // Sprinkles "ID": "Platonymous.Sprinkles", "Defaults": { "ChucklefishID": 4592 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -952,7 +952,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Sprint and Dash", + // Sprint and Dash "ID": "SPDSprintAndDash", "Defaults": { "ChucklefishID": 3531 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -961,7 +961,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Sprint and Dash Redux", + // Sprint and Dash Redux "ID": "SPDSprintAndDash", "Defaults": { "ChucklefishID": 4201 }, "Compatibility": { @@ -969,7 +969,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Sprinting Mod", + // Sprinting Mod "ID": "a10d3097-b073-4185-98ba-76b586cba00c", "Defaults": { "GitHubProject": "oliverpl/SprintingMod" }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -978,7 +978,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "StackSplitX", + // StackSplitX "ID": "StackSplitX.dll", "Defaults": { "NexusID": 798 }, "Compatibility": { @@ -986,7 +986,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "StaminaRegen", + // StaminaRegen "ID": "StaminaRegen.dll", "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { @@ -994,7 +994,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Stardew Auto Backup", + // Stardew Auto Backup "ID": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'Stardew_Save_Backup'}", // disambiguate from other Alpha_Omegasis mods "Defaults": { "NexusID": 435 }, "Compatibility": { @@ -1002,7 +1002,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Stardew Notification", + // Stardew Notification "ID": "stardewnotification", "Defaults": { "GitHubProject": "monopandora/StardewNotification" }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -1011,7 +1011,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Stardew Symphony", + // Stardew Symphony "ID": "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'Stardew_Symphony'}", // disambiguate other mods by Alpha_Omegasis "Defaults": { "NexusID": 425 }, "Compatibility": { @@ -1019,14 +1019,14 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "StarDustCore", + // StarDustCore "ID": "StarDustCore", "Compatibility": { "~": { "Status": "Obsolete", "ReasonPhrase": "it was only used by earlier versions of Save Anywhere, and is no longer used or maintained." } } }, { - "Name": "StashItemsToChest", + // StashItemsToChest "ID": "BlueMod_StashItemsToChest", "Defaults": { "GitHubProject": "lambui/StardewValleyMod_StashItemsToChest" }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -1035,7 +1035,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Stone Bridge Over Pond (PondWithBridge)", + // Stone Bridge Over Pond (PondWithBridge) "ID": "PondWithBridge.dll", "Defaults": { "NexusID": 316 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -1044,7 +1044,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Super Greenhouse Warp Modifier", + // Super Greenhouse Warp Modifier "ID": "SuperGreenhouse", "Defaults": { "ChucklefishID": 4334 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -1053,7 +1053,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Tainted Cellar", + // Tainted Cellar "ID": "TaintedCellar.dll", "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { @@ -1061,7 +1061,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Teleporter", + // Teleporter "ID": "Teleporter", "Defaults": { "ChucklefishID": 4374 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -1070,7 +1070,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Three-heart Dance Partner", + // Three-heart Dance Partner "ID": "ThreeHeartDancePartner", "Defaults": { "NexusID": 500 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -1079,7 +1079,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "TimeSpeed", + // TimeSpeed "ID": "TimeSpeed.dll | {ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'TimeSpeed'} | {ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'TimeSpeed Mod (unofficial)'} | community.TimeSpeed", // changed in 2.0.3 and 2.1; disambiguate other mods by Alpha_Omegasis "Defaults": { "NexusID": 169 }, "Compatibility": { @@ -1087,7 +1087,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "UiModSuite", + // UiModSuite "ID": "Demiacle.UiModSuite", "Defaults": { "NexusID": 1023 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -1096,7 +1096,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "WakeUp", + // WakeUp "ID": "WakeUp.dll", "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { @@ -1104,7 +1104,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Wallpaper Fix", + // Wallpaper Fix "ID": "WallpaperFix.dll", "Defaults": { "ChucklefishID": 4211 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -1113,7 +1113,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Weather Controller", + // Weather Controller "ID": "WeatherController.dll", "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { @@ -1121,7 +1121,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Wonderful Farm Life", + // Wonderful Farm Life "ID": "WonderfulFarmLife.dll", "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { @@ -1129,14 +1129,14 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "XmlSerializerRetool", + // XmlSerializerRetool "ID": "XmlSerializerRetool.dll", "Compatibility": { "~": { "Status": "Obsolete", "ReasonPhrase": "it's no longer maintained or used." } } }, { - "Name": "Xnb Loader", + // Xnb Loader "ID": "Entoarox.XnbLoader", "Defaults": { "ChucklefishID": 4506 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -1145,7 +1145,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "zDailyIncrease", + // zDailyIncrease "ID": "zdailyincrease", "Defaults": { "ChucklefishID": 4247 }, "Compatibility": { @@ -1153,7 +1153,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Zoom Out Extreme", + // Zoom Out Extreme "ID": "ZoomMod", "AlternativeUrl": "http://community.playstarbound.com/threads/115028", "Compatibility": { @@ -1161,7 +1161,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Zoryn's Better RNG", + // Zoryn's Better RNG "ID": "76b6d1e1-f7ba-4d72-8c32-5a1e6d2716f6 | Zoryn.BetterRNG", // changed in 1.6 "Defaults": { "GitHubProject": "Zoryn4163/SMAPI-Mods" }, "Compatibility": { @@ -1169,7 +1169,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Zoryn's Calendar Anywhere", + // Zoryn's Calendar Anywhere "ID": "a41c01cd-0437-43eb-944f-78cb5a53002a | Zoryn.CalendarAnywhere", // changed in 1.6 "Defaults": { "GitHubProject": "Zoryn4163/SMAPI-Mods" }, "Compatibility": { @@ -1177,7 +1177,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Zoryn's Health Bars", + // Zoryn's Health Bars "ID": "HealthBars.dll | Zoryn.HealthBars", // changed in 1.6 "Defaults": { "GitHubProject": "Zoryn4163/SMAPI-Mods" }, "Compatibility": { @@ -1185,7 +1185,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Zoryn's Junimo Deposit Anywhere", + // Zoryn's Junimo Deposit Anywhere "ID": "f93a4fe8-cade-4146-9335-b5f82fbbf7bc | Zoryn.JunimoDepositAnywhere", // changed in 1.6 "Defaults": { "GitHubProject": "Zoryn4163/SMAPI-Mods" }, "Compatibility": { @@ -1193,7 +1193,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Zoryn's Movement Mod", + // Zoryn's Movement Mod "ID": "8a632929-8335-484f-87dd-c29d2ba3215d | Zoryn.MovementModifier", // changed in 1.6 "Defaults": { "GitHubProject": "Zoryn4163/SMAPI-Mods" }, "Compatibility": { @@ -1201,7 +1201,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - "Name": "Zoryn's Regen Mod", + // Zoryn's Regen Mod "ID": "dfac4383-1b6b-4f33-ae4e-37fc23e5252e | Zoryn.RegenMod", // changed in 1.6 "Defaults": { "GitHubProject": "Zoryn4163/SMAPI-Mods" }, "Compatibility": { From 96acccad7c57b005f73fbe6aad30291c7fbb633d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 Sep 2017 12:13:34 -0400 Subject: [PATCH 107/186] bug fixes, improve update-check logging (#361) --- src/StardewModdingAPI/Program.cs | 49 ++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index fcfa1efc..5ecc5634 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -334,8 +334,7 @@ namespace StardewModdingAPI this.Monitor.Log($"You configured SMAPI to not check for updates. Running an old version of SMAPI is not recommended. You can enable update checks by reinstalling SMAPI or editing {Constants.ApiConfigPath}.", LogLevel.Warn); if (!this.Monitor.WriteToConsole) this.Monitor.Log("Writing to the terminal is disabled because the --no-terminal argument was received. This usually means launching the terminal failed.", LogLevel.Warn); - if (this.Settings.VerboseLogging) - this.Monitor.Log("Verbose logging enabled.", LogLevel.Trace); + this.VerboseLog("Verbose logging enabled."); // validate XNB integrity if (!this.ValidateContentIntegrity()) @@ -490,6 +489,8 @@ namespace StardewModdingAPI new Thread(() => { + this.Monitor.Log("Checking for updates...", LogLevel.Trace); + // update info List updates = new List(); bool smapiUpdate = false; @@ -508,7 +509,10 @@ namespace StardewModdingAPI { smapiUpdate = true; updates.Add($"SMAPI {response.Version}: {response.Url}"); + this.VerboseLog($" SMAPI: update to {response.Version} found."); } + else + this.VerboseLog(" SMAPI: OK."); } catch (Exception ex) { @@ -522,12 +526,34 @@ namespace StardewModdingAPI IDictionary modsByKey = new Dictionary(StringComparer.InvariantCultureIgnoreCase); foreach (IModMetadata mod in mods) { + // validate + if (mod.Manifest == null) + { + this.VerboseLog($" {mod.DisplayName}: no manifest."); + continue; + } + + // add update keys + bool hasUpdateKeys = false; if (!string.IsNullOrWhiteSpace(mod.Manifest.ChucklefishID)) + { + hasUpdateKeys = true; modsByKey[$"Chucklefish:{mod.Manifest.ChucklefishID}"] = mod; + } if (!string.IsNullOrWhiteSpace(mod.Manifest.NexusID)) + { + hasUpdateKeys = true; modsByKey[$"Nexus:{mod.Manifest.NexusID}"] = mod; + } if (!string.IsNullOrWhiteSpace(mod.Manifest.GitHubProject)) + { + hasUpdateKeys = true; modsByKey[$"GitHub:{mod.Manifest.GitHubProject}"] = mod; + } + + // log + if (!hasUpdateKeys) + this.VerboseLog($" {mod.DisplayName}: no update keys."); } // fetch results @@ -535,17 +561,20 @@ namespace StardewModdingAPI IDictionary updatesByMod = new Dictionary(); foreach (var entry in response) { + IModMetadata mod = modsByKey[entry.Key]; + // handle error if (entry.Value.Error != null) { - this.Monitor.Log($"Couldn't fetch version of {modsByKey[entry.Key].DisplayName} with key {entry.Key}:\n{entry.Value.Error}", LogLevel.Trace); + this.Monitor.Log($" {mod.DisplayName} ({entry.Key}): update error: {entry.Value.Error}", LogLevel.Trace); continue; } - // collect latest mod version - IModMetadata mod = modsByKey[entry.Key]; + // track update ISemanticVersion version = new SemanticVersion(entry.Value.Version); - if (version.IsNewerThan(mod.Manifest.Version)) + bool isUpdate = version.IsNewerThan(mod.Manifest.Version); + this.VerboseLog($" {mod.DisplayName} ({entry.Key}): {(isUpdate ? $"update to {entry.Value.Version} found" : "OK")}."); + if (isUpdate) { if (!updatesByMod.TryGetValue(mod, out ModInfoModel other) || version.IsNewerThan(other.Version)) { @@ -942,5 +971,13 @@ namespace StardewModdingAPI #endif return Environment.OSVersion.ToString(); } + + /// Log a message if verbose mode is enabled. + /// The message to log. + private void VerboseLog(string message) + { + if (this.Settings.VerboseLogging) + this.Monitor.Log(message, LogLevel.Trace); + } } } From 9b247b67f64af903fd610481e74dbe7175580b84 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 Sep 2017 13:19:02 -0400 Subject: [PATCH 108/186] improve version normalising (#336, #361) --- .../Controllers/ModsController.cs | 6 ++- .../ModRepositories/BaseRepository.cs | 51 +++++++++++++++++++ .../ModRepositories/ChucklefishRepository.cs | 27 ++++------ .../ModRepositories/GitHubRepository.cs | 32 +++--------- .../ModRepositories/NexusRepository.cs | 23 +++------ 5 files changed, 81 insertions(+), 58 deletions(-) create mode 100644 src/StardewModdingAPI.Web/Framework/ModRepositories/BaseRepository.cs diff --git a/src/StardewModdingAPI.Web/Controllers/ModsController.cs b/src/StardewModdingAPI.Web/Controllers/ModsController.cs index 4eaa66d2..566577e4 100644 --- a/src/StardewModdingAPI.Web/Controllers/ModsController.cs +++ b/src/StardewModdingAPI.Web/Controllers/ModsController.cs @@ -108,9 +108,11 @@ namespace StardewModdingAPI.Web.Controllers result[modKey] = await this.Cache.GetOrCreateAsync($"{repository.VendorKey}:{modID}".ToLower(), async entry => { entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(this.CacheMinutes); + ModInfoModel info = await repository.GetModInfoAsync(modID); - if (info.Error == null && !Regex.IsMatch(info.Version, this.VersionRegex, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)) - info = new ModInfoModel(info.Name, info.Version, info.Url, $"Mod has invalid semantic version '{info.Version}'."); + if (info.Error == null && (info.Version == null || !Regex.IsMatch(info.Version, this.VersionRegex, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase))) + info = new ModInfoModel(info.Name, info.Version, info.Url, info.Version == null ? "Mod has no version number." : $"Mod has invalid semantic version '{info.Version}'."); + return info; }); } diff --git a/src/StardewModdingAPI.Web/Framework/ModRepositories/BaseRepository.cs b/src/StardewModdingAPI.Web/Framework/ModRepositories/BaseRepository.cs new file mode 100644 index 00000000..d98acd89 --- /dev/null +++ b/src/StardewModdingAPI.Web/Framework/ModRepositories/BaseRepository.cs @@ -0,0 +1,51 @@ +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using StardewModdingAPI.Models; + +namespace StardewModdingAPI.Web.Framework.ModRepositories +{ + internal abstract class RepositoryBase : IModRepository + { + /********* + ** Accessors + *********/ + /// The unique key for this vendor. + public string VendorKey { get; } + + + /********* + ** Public methods + *********/ + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public abstract void Dispose(); + + /// Get metadata about a mod in the repository. + /// The mod ID in this repository. + public abstract Task GetModInfoAsync(string id); + + + /********* + ** Protected methods + *********/ + /// Construct an instance. + /// The unique key for this vendor. + protected RepositoryBase(string vendorKey) + { + this.VendorKey = vendorKey; + } + + /// Normalise a version string. + /// The version to normalise. + protected string NormaliseVersion(string version) + { + if (string.IsNullOrWhiteSpace(version)) + return null; + + version = version.Trim(); + if (Regex.IsMatch(version, @"^v\d", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)) // common version prefix + version = version.Substring(1); + + return version; + } + } +} diff --git a/src/StardewModdingAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs b/src/StardewModdingAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs index 59d7f3ba..4822c414 100644 --- a/src/StardewModdingAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs +++ b/src/StardewModdingAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs @@ -8,26 +8,19 @@ using StardewModdingAPI.Models; namespace StardewModdingAPI.Web.Framework.ModRepositories { /// An HTTP client for fetching mod metadata from the Chucklefish mod site. - internal class ChucklefishRepository : IModRepository + internal class ChucklefishRepository : RepositoryBase { /********* ** Properties *********/ - /// The underlying HTTP client. - private readonly IClient Client; - - - /********* - ** Accessors - *********/ - /// The unique key for this vendor. - public string VendorKey { get; } - /// The base URL for the Chucklefish mod site. - public string BaseUrl { get; } + private readonly string BaseUrl; /// The URL for a mod page excluding the base URL, where {0} is the mod ID. - public string ModPageUrlFormat { get; } + private readonly string ModPageUrlFormat; + + /// The underlying HTTP client. + private readonly IClient Client; /********* @@ -39,8 +32,8 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories /// The base URL for the Chucklefish mod site. /// The URL for a mod page excluding the , where {0} is the mod ID. public ChucklefishRepository(string vendorKey, string userAgent, string baseUrl, string modPageUrlFormat) + : base(vendorKey) { - this.VendorKey = vendorKey; this.BaseUrl = baseUrl; this.ModPageUrlFormat = modPageUrlFormat; this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent); @@ -48,7 +41,7 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories /// Get metadata about a mod in the repository. /// The mod ID in this repository. - public async Task GetModInfoAsync(string id) + public override async Task GetModInfoAsync(string id) { try { @@ -77,7 +70,7 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories string version = doc.DocumentNode.SelectSingleNode("//h1/span").InnerText; // create model - return new ModInfoModel(name, version, url); + return new ModInfoModel(name, this.NormaliseVersion(version), url); } catch (Exception ex) { @@ -86,7 +79,7 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories } /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - public void Dispose() + public override void Dispose() { this.Client.Dispose(); } diff --git a/src/StardewModdingAPI.Web/Framework/ModRepositories/GitHubRepository.cs b/src/StardewModdingAPI.Web/Framework/ModRepositories/GitHubRepository.cs index f794c605..7dfe9f62 100644 --- a/src/StardewModdingAPI.Web/Framework/ModRepositories/GitHubRepository.cs +++ b/src/StardewModdingAPI.Web/Framework/ModRepositories/GitHubRepository.cs @@ -7,25 +7,18 @@ using StardewModdingAPI.Models; namespace StardewModdingAPI.Web.Framework.ModRepositories { /// An HTTP client for fetching mod metadata from GitHub project releases. - internal class GitHubRepository : IModRepository + internal class GitHubRepository : RepositoryBase { /********* ** Properties *********/ + /// The URL for a Nexus Mods API query excluding the base URL, where {0} is the mod ID. + private readonly string ReleaseUrlFormat; + /// The underlying HTTP client. private readonly IClient Client; - /********* - ** Accessors - *********/ - /// The unique key for this vendor. - public string VendorKey { get; } - - /// The URL for a Nexus Mods API query excluding the base URL, where {0} is the mod ID. - public string ReleaseUrlFormat { get; } - - /********* ** Public methods *********/ @@ -38,8 +31,8 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories /// The username with which to authenticate to the GitHub API. /// The password with which to authenticate to the GitHub API. public GitHubRepository(string vendorKey, string baseUrl, string releaseUrlFormat, string userAgent, string acceptHeader, string username, string password) + : base(vendorKey) { - this.VendorKey = vendorKey; this.ReleaseUrlFormat = releaseUrlFormat; this.Client = new FluentClient(baseUrl) @@ -51,23 +44,14 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories /// Get metadata about a mod in the repository. /// The mod ID in this repository. - public async Task GetModInfoAsync(string id) + public override async Task GetModInfoAsync(string id) { try { - // fetch data GitRelease release = await this.Client .GetAsync(string.Format(this.ReleaseUrlFormat, id)) .As(); - - // extract fields - string name = id; - string version = release.Tag; - if (version.StartsWith("v")) // common format on GitHub - version = version.Substring(1); - string url = $"https://github.com/{id}/releases"; - - return new ModInfoModel(name, version, url); + return new ModInfoModel(id, this.NormaliseVersion(release.Tag), $"https://github.com/{id}/releases"); } catch (Exception ex) { @@ -76,7 +60,7 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories } /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - public void Dispose() + public override void Dispose() { this.Client.Dispose(); } diff --git a/src/StardewModdingAPI.Web/Framework/ModRepositories/NexusRepository.cs b/src/StardewModdingAPI.Web/Framework/ModRepositories/NexusRepository.cs index 6cf5b04a..e679b977 100644 --- a/src/StardewModdingAPI.Web/Framework/ModRepositories/NexusRepository.cs +++ b/src/StardewModdingAPI.Web/Framework/ModRepositories/NexusRepository.cs @@ -7,25 +7,18 @@ using StardewModdingAPI.Models; namespace StardewModdingAPI.Web.Framework.ModRepositories { /// An HTTP client for fetching mod metadata from Nexus Mods. - internal class NexusRepository : IModRepository + internal class NexusRepository : RepositoryBase { /********* ** Properties *********/ + /// The URL for a Nexus Mods API query excluding the base URL, where {0} is the mod ID. + private readonly string ModUrlFormat; + /// The underlying HTTP client. private readonly IClient Client; - /********* - ** Accessors - *********/ - /// The unique key for this vendor. - public string VendorKey { get; } - - /// The URL for a Nexus Mods API query excluding the base URL, where {0} is the mod ID. - public string ModUrlFormat { get; } - - /********* ** Public methods *********/ @@ -35,15 +28,15 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories /// The base URL for the Nexus Mods API. /// The URL for a Nexus Mods API query excluding the , where {0} is the mod ID. public NexusRepository(string vendorKey, string userAgent, string baseUrl, string modUrlFormat) + : base(vendorKey) { - this.VendorKey = vendorKey; this.ModUrlFormat = modUrlFormat; this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent); } /// Get metadata about a mod in the repository. /// The mod ID in this repository. - public async Task GetModInfoAsync(string id) + public override async Task GetModInfoAsync(string id) { try { @@ -52,7 +45,7 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories .As(); return response != null - ? new ModInfoModel(response.Name, response.Version, response.Url) + ? new ModInfoModel(response.Name, this.NormaliseVersion(response.Version), response.Url) : new ModInfoModel("Found no mod with this ID."); } catch (Exception ex) @@ -62,7 +55,7 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories } /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - public void Dispose() + public override void Dispose() { this.Client.Dispose(); } From 021e1a278b05d1ba177b3759e534cc372fdc8548 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 Sep 2017 13:35:26 -0400 Subject: [PATCH 109/186] list SMAPI update separately (#336, #361) --- src/StardewModdingAPI/Program.cs | 57 ++++++++++---------------------- 1 file changed, 18 insertions(+), 39 deletions(-) diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 5ecc5634..8a337562 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -489,40 +489,36 @@ namespace StardewModdingAPI new Thread(() => { - this.Monitor.Log("Checking for updates...", LogLevel.Trace); - - // update info - List updates = new List(); - bool smapiUpdate = false; - int modUpdates = 0; - // create client WebApiClient client = new WebApiClient(this.Settings.WebApiBaseUrl, Constants.ApiVersion); - // fetch SMAPI version + // check SMAPI version try { + this.Monitor.Log("Checking for SMAPI update...", LogLevel.Trace); + ModInfoModel response = client.GetModInfoAsync($"GitHub:{this.Settings.GitHubProjectName}").Result.Single().Value; if (response.Error != null) - this.Monitor.Log($"Couldn't check for a new version of SMAPI. This won't affect your game, but you may not be notified of new versions if this keeps happening.\n{response.Error}", LogLevel.Warn); - else if (new SemanticVersion(response.Version).IsNewerThan(Constants.ApiVersion)) { - smapiUpdate = true; - updates.Add($"SMAPI {response.Version}: {response.Url}"); - this.VerboseLog($" SMAPI: update to {response.Version} found."); + this.Monitor.Log("Couldn't check for a new version of SMAPI. This won't affect your game, but you may not be notified of new versions if this keeps happening.", LogLevel.Warn); + this.Monitor.Log($"Error: {response.Error}"); } + else if (new SemanticVersion(response.Version).IsNewerThan(Constants.ApiVersion)) + this.Monitor.Log($"You can update SMAPI to {response.Version}: {response.Url}", LogLevel.Alert); else - this.VerboseLog(" SMAPI: OK."); + this.VerboseLog(" OK."); } catch (Exception ex) { - this.Monitor.Log($"Couldn't check for a new version of SMAPI. This won't affect your game, but you may not be notified of new versions if this keeps happening.\n{ex.GetLogSummary()}"); + this.Monitor.Log("Couldn't check for a new version of SMAPI. This won't affect your game, but you may not be notified of new versions if this keeps happening.", LogLevel.Warn); + this.Monitor.Log($"Error: {ex.GetLogSummary()}"); } - // fetch mod versions + // check mod versions try { - // prepare update-check data + // prepare update keys + this.VerboseLog("Collecting mod update keys..."); IDictionary modsByKey = new Dictionary(StringComparer.InvariantCultureIgnoreCase); foreach (IModMetadata mod in mods) { @@ -557,6 +553,7 @@ namespace StardewModdingAPI } // fetch results + this.Monitor.Log($"Checking for updates to {modsByKey.Count} keys...", LogLevel.Trace); IDictionary response = client.GetModInfoAsync(modsByKey.Keys.ToArray()).Result; IDictionary updatesByMod = new Dictionary(); foreach (var entry in response) @@ -577,18 +574,17 @@ namespace StardewModdingAPI if (isUpdate) { if (!updatesByMod.TryGetValue(mod, out ModInfoModel other) || version.IsNewerThan(other.Version)) - { updatesByMod[mod] = entry.Value; - modUpdates++; - } } } - // add to output queue + // output if (updatesByMod.Any()) { + this.Monitor.Newline(); + this.Monitor.Log($"You can update {updatesByMod.Count} mod{(updatesByMod.Count != 1 ? "s" : "")}:", LogLevel.Alert); foreach (var entry in updatesByMod.OrderBy(p => p.Key.DisplayName)) - updates.Add($"{entry.Key.DisplayName} {entry.Value.Version}: {entry.Value.Url}"); + this.Monitor.Log($" {entry.Key.DisplayName} {entry.Value.Version}: {entry.Value.Url}", LogLevel.Alert); } } catch (Exception ex) @@ -596,24 +592,7 @@ namespace StardewModdingAPI this.Monitor.Log($"Couldn't check for new mod versions:\n{ex.GetLogSummary()}", LogLevel.Trace); } - // output - if (updates.Any()) - { - this.Monitor.Newline(); - // print intro - string intro = ""; - if (smapiUpdate) - intro = "You can update SMAPI"; - if (modUpdates > 0) - intro += $"{(smapiUpdate ? " and" : "You can update")} {modUpdates} mod{(modUpdates != 1 ? "s" : "")}"; - intro += ":"; - this.Monitor.Log(intro, LogLevel.Alert); - - // print update list - foreach (string line in updates) - this.Monitor.Log($" {line}", LogLevel.Alert); - } }).Start(); } From cb1f11a8462f3db34812ecbfb06227d3a2081d7f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 Sep 2017 14:10:36 -0400 Subject: [PATCH 110/186] update config documentation (#361) --- README.md | 16 ++++------- .../StardewModdingAPI.config.json | 28 ++++++++++++++++--- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 7a5f9009..693530d1 100644 --- a/README.md +++ b/README.md @@ -144,18 +144,12 @@ game folder. Basic fields: -field | purpose ------ | ------- -`DeveloperMode` | Default `false` (except in _SMAPI for developers_ releases). Whether to enable features intended for mod developers (mainly more detailed console logging). +field | purpose +----------------- | ------- +`DeveloperMode` | Default `false` (except in _SMAPI for developers_ releases). Whether to enable features intended for mod developers (mainly more detailed console logging). `CheckForUpdates` | Default `true`. Whether SMAPI should check for a newer version when you load the game. If a new version is available, a small message will appear in the console. This doesn't affect the load time even if your connection is offline or slow, because it happens in the background. -`VerboseLogging` | Default `false`. Whether SMAPI should log more information about the game context. - -Advanced fields (changing these isn't recommended and may destabilise your game): - -field | purpose ------ | ------- -`DisabledMods` | A list of mods to consider obsolete and not load. -`ModCompatibility` | A list of mod versions SMAPI should consider compatible or broken regardless of whether it detects incompatible code. This can be used to force SMAPI to load an incompatible mod, though that isn't recommended. +`VerboseLogging` | Default `false`. Whether SMAPI should log more information about the game context. +`ModData` | Internal metadata about SMAPI mods. Changing this isn't recommended and may destabilise your game. See documentation in the file. ### Command-line arguments The SMAPI installer recognises three command-line arguments: diff --git a/src/StardewModdingAPI/StardewModdingAPI.config.json b/src/StardewModdingAPI/StardewModdingAPI.config.json index de7efcf8..3d156a15 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.config.json +++ b/src/StardewModdingAPI/StardewModdingAPI.config.json @@ -39,10 +39,30 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "VerboseLogging": false, /** - * A list of mod versions SMAPI should consider compatible or broken regardless of whether it - * detects incompatible code. The default for each record is to assume broken; to force SMAPI to - * load a mod regardless of compatibility checks, add a "Compatibility": "AssumeCompatible" field. - * Changing this field is not recommended and may destabilise your game. + * Extra metadata about some SMAPI mods. All fields except 'ID' are optional. + * + * - 'ID' uniquely identifies the mod across all versions, even if its manifest fields changed or + * the mod doesn't have a unique ID. The format is as follows: + * - If the mod's identifier changed over time, multiple variants are separated by |. + * - Each variant can take one of two forms: a simple string matching the mod's UniqueID, + * or a JSON structure containing any of three manifest fields (ID, Name, and Author) to + * match. + * + * - 'Defaults' specifies fields to inject into the mod's manifest if they're not already set. + * Supported fields: ChucklefishID, GitHubProject, and NexusID. + * + * - 'AlternativeUrl' specifies a URL where the player can find an unofficial update or + * alternative if the mod is no longer compatible. + * + * - 'Compatibility' overrides SMAPI's normal compatibility detection. The keys are version + * ranges in the form lower~upper, where either side can be blank for an unbounded range. (For + * example, "~1.0" means all versions up to 1.0 inclusively.) The values have two fields: + * - 'Status' specifies the compatibility. Valid values are Obsolete (SMAPI won't load it + * because the mod should no longer be used), AssumeBroken (SMAPI won't load it because + * the specified version isn't compatible), or AssumeCompatible (SMAPI will load it even + * if it detects incompatible code). + * - 'ReasonPhrase' (optional) specifies a message to show to the player explaining why the + * mod isn't loaded. This has no effect for AssumeCompatible. */ "ModData": [ { From f84def385d3d1b537e7f19b6ae1f90096e136505 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 Sep 2017 17:44:56 -0400 Subject: [PATCH 111/186] sort update-check trace logs (#361) --- src/StardewModdingAPI/Program.cs | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 8a337562..3e1db20c 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -554,27 +554,35 @@ namespace StardewModdingAPI // fetch results this.Monitor.Log($"Checking for updates to {modsByKey.Count} keys...", LogLevel.Trace); - IDictionary response = client.GetModInfoAsync(modsByKey.Keys.ToArray()).Result; + var results = + ( + from entry in client.GetModInfoAsync(modsByKey.Keys.ToArray()).Result + let mod = modsByKey[entry.Key] + orderby mod.DisplayName + select new { entry.Key, Mod = modsByKey[entry.Key], Info = entry.Value } + ) + .ToArray(); IDictionary updatesByMod = new Dictionary(); - foreach (var entry in response) + foreach (var result in results) { - IModMetadata mod = modsByKey[entry.Key]; + IModMetadata mod = result.Mod; + ModInfoModel info = result.Info; // handle error - if (entry.Value.Error != null) + if (info.Error != null) { - this.Monitor.Log($" {mod.DisplayName} ({entry.Key}): update error: {entry.Value.Error}", LogLevel.Trace); + this.Monitor.Log($" {mod.DisplayName} ({result.Key}): update error: {info.Error}", LogLevel.Trace); continue; } // track update - ISemanticVersion version = new SemanticVersion(entry.Value.Version); + ISemanticVersion version = new SemanticVersion(info.Version); bool isUpdate = version.IsNewerThan(mod.Manifest.Version); - this.VerboseLog($" {mod.DisplayName} ({entry.Key}): {(isUpdate ? $"update to {entry.Value.Version} found" : "OK")}."); + this.VerboseLog($" {mod.DisplayName} ({result.Key}): {(isUpdate ? $"{mod.Manifest.Version} => {info.Version}" : "OK")}."); if (isUpdate) { if (!updatesByMod.TryGetValue(mod, out ModInfoModel other) || version.IsNewerThan(other.Version)) - updatesByMod[mod] = entry.Value; + updatesByMod[mod] = info; } } From b105c97dda01441d503d31e8b8ac0b3fd35fef14 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 Sep 2017 19:55:34 -0400 Subject: [PATCH 112/186] add support for remapping legacy versions for update checks (#361) --- .../Framework/Models/ModDataRecord.cs | 25 +++++++++++++++++++ src/StardewModdingAPI/Program.cs | 9 ++++--- .../StardewModdingAPI.config.json | 5 ++++ 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/StardewModdingAPI/Framework/Models/ModDataRecord.cs b/src/StardewModdingAPI/Framework/Models/ModDataRecord.cs index a2397fa8..0d033e82 100644 --- a/src/StardewModdingAPI/Framework/Models/ModDataRecord.cs +++ b/src/StardewModdingAPI/Framework/Models/ModDataRecord.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; using StardewModdingAPI.Framework.Serialisation; @@ -24,6 +25,12 @@ namespace StardewModdingAPI.Framework.Models [JsonConverter(typeof(SFieldConverter))] public ModCompatibility[] Compatibility { get; set; } = new ModCompatibility[0]; + /// Map local versions to a semantic version for update checks. + public IDictionary MapLocalVersions { get; set; } = new Dictionary(); + + /// Map remote versions to a semantic version for update checks. + public IDictionary MapRemoteVersions { get; set; } = new Dictionary(); + /********* ** Public methods @@ -34,5 +41,23 @@ namespace StardewModdingAPI.Framework.Models { return this.Compatibility.FirstOrDefault(p => p.MatchesVersion(version)); } + + /// Get a semantic local version for update checks. + /// The local version to normalise. + public string GetLocalVersionForUpdateChecks(string version) + { + return this.MapLocalVersions != null && this.MapLocalVersions.TryGetValue(version, out string newVersion) + ? newVersion + : version; + } + + /// Get a semantic remote version for update checks. + /// The remote version to normalise. + public string GetRemoteVersionForUpdateChecks(string version) + { + return this.MapRemoteVersions != null && this.MapRemoteVersions.TryGetValue(version, out string newVersion) + ? newVersion + : version; + } } } diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 3e1db20c..7b3048c0 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -576,12 +576,13 @@ namespace StardewModdingAPI } // track update - ISemanticVersion version = new SemanticVersion(info.Version); - bool isUpdate = version.IsNewerThan(mod.Manifest.Version); - this.VerboseLog($" {mod.DisplayName} ({result.Key}): {(isUpdate ? $"{mod.Manifest.Version} => {info.Version}" : "OK")}."); + ISemanticVersion localVersion = new SemanticVersion(mod.DataRecord?.GetLocalVersionForUpdateChecks(mod.Manifest.Version.ToString())); + ISemanticVersion latestVersion = new SemanticVersion(mod.DataRecord?.GetRemoteVersionForUpdateChecks(info.Version)); + bool isUpdate = latestVersion.IsNewerThan(localVersion); + this.VerboseLog($" {mod.DisplayName} ({result.Key}): {(isUpdate ? $"{mod.Manifest.Version}{(!localVersion.Equals(mod.Manifest.Version) ? $" [{localVersion}]" : "")} => {info.Version}{(!latestVersion.Equals(new SemanticVersion(info.Version)) ? $" [{latestVersion}]" : "")}" : "OK")}."); if (isUpdate) { - if (!updatesByMod.TryGetValue(mod, out ModInfoModel other) || version.IsNewerThan(other.Version)) + if (!updatesByMod.TryGetValue(mod, out ModInfoModel other) || latestVersion.IsNewerThan(other.Version)) updatesByMod[mod] = info; } } diff --git a/src/StardewModdingAPI/StardewModdingAPI.config.json b/src/StardewModdingAPI/StardewModdingAPI.config.json index 3d156a15..a768c762 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.config.json +++ b/src/StardewModdingAPI/StardewModdingAPI.config.json @@ -63,6 +63,11 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha * if it detects incompatible code). * - 'ReasonPhrase' (optional) specifies a message to show to the player explaining why the * mod isn't loaded. This has no effect for AssumeCompatible. + * + * - 'MapLocalVersions' and 'MapRemoteVersions' substitute versions for update checks. For + * example, if the API returns version '1.1-1078', MapRemoteVersions can map it to '1.1' when + * comparing to the mod's current version. This is only intended to support legacy mods with + * injected update keys. */ "ModData": [ { From 6dff9779a349945d502dee67d5d4dd8e63b4f753 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 25 Sep 2017 17:39:51 -0400 Subject: [PATCH 113/186] use POST for SMAPI update checks to avoid issues with long queries (#336) --- src/StardewModdingAPI.Models/ModSeachModel.cs | 30 +++++++++++++++++++ .../StardewModdingAPI.Models.projitems | 1 + .../Controllers/ModsController.cs | 17 +++++++++-- .../Framework/VersionConstraint.cs | 15 ++++++++++ src/StardewModdingAPI.Web/Startup.cs | 19 ++---------- .../Framework/WebApiClient.cs | 29 ++++++++++++++---- 6 files changed, 87 insertions(+), 24 deletions(-) create mode 100644 src/StardewModdingAPI.Models/ModSeachModel.cs create mode 100644 src/StardewModdingAPI.Web/Framework/VersionConstraint.cs diff --git a/src/StardewModdingAPI.Models/ModSeachModel.cs b/src/StardewModdingAPI.Models/ModSeachModel.cs new file mode 100644 index 00000000..526fbaf3 --- /dev/null +++ b/src/StardewModdingAPI.Models/ModSeachModel.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using System.Linq; + +namespace StardewModdingAPI.Models +{ + /// Specifies mods whose update-check info to fetch. + internal class ModSearchModel + { + /********* + ** Accessors + *********/ + /// The namespaced mod keys to search. + public string[] ModKeys { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an empty instance. + /// This constructed is needed for JSON deserialisation. + public ModSearchModel() { } + + /// Construct an valid instance. + /// The namespaced mod keys to search. + public ModSearchModel(IEnumerable modKeys) + { + this.ModKeys = modKeys.ToArray(); + } + } +} diff --git a/src/StardewModdingAPI.Models/StardewModdingAPI.Models.projitems b/src/StardewModdingAPI.Models/StardewModdingAPI.Models.projitems index 2465760e..e2cb29e1 100644 --- a/src/StardewModdingAPI.Models/StardewModdingAPI.Models.projitems +++ b/src/StardewModdingAPI.Models/StardewModdingAPI.Models.projitems @@ -9,6 +9,7 @@ StardewModdingAPI.Models + \ No newline at end of file diff --git a/src/StardewModdingAPI.Web/Controllers/ModsController.cs b/src/StardewModdingAPI.Web/Controllers/ModsController.cs index 566577e4..f29de45a 100644 --- a/src/StardewModdingAPI.Web/Controllers/ModsController.cs +++ b/src/StardewModdingAPI.Web/Controllers/ModsController.cs @@ -14,6 +14,7 @@ namespace StardewModdingAPI.Web.Controllers { /// Provides an API to perform mod update checks. [Produces("application/json")] + [Route("api/{version:semanticVersion}/[controller]")] internal class ModsController : Controller { /********* @@ -79,16 +80,28 @@ namespace StardewModdingAPI.Web.Controllers /// The namespaced mod keys to search as a comma-delimited array. [HttpGet] public async Task> GetAsync(string modKeys) + { + string[] modKeysArray = modKeys?.Split(',').Select(p => p.Trim()).ToArray(); + if (modKeysArray == null || !modKeysArray.Any()) + return new Dictionary(); + + return await this.PostAsync(new ModSearchModel(modKeysArray)); + } + + /// Fetch version metadata for the given mods. + /// The mod search criteria. + [HttpPost] + public async Task> PostAsync([FromBody] ModSearchModel search) { // sort & filter keys - string[] modKeysArray = (modKeys?.Split(',').Select(p => p.Trim()).ToArray() ?? new string[0]) + string[] modKeys = (search?.ModKeys?.ToArray() ?? new string[0]) .Distinct(StringComparer.CurrentCultureIgnoreCase) .OrderBy(p => p, StringComparer.CurrentCultureIgnoreCase) .ToArray(); // fetch mod info IDictionary result = new Dictionary(StringComparer.CurrentCultureIgnoreCase); - foreach (string modKey in modKeysArray) + foreach (string modKey in modKeys) { // parse mod key if (!this.TryParseModKey(modKey, out string vendorKey, out string modID)) diff --git a/src/StardewModdingAPI.Web/Framework/VersionConstraint.cs b/src/StardewModdingAPI.Web/Framework/VersionConstraint.cs new file mode 100644 index 00000000..be9c0918 --- /dev/null +++ b/src/StardewModdingAPI.Web/Framework/VersionConstraint.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Routing.Constraints; + +namespace StardewModdingAPI.Web.Framework +{ + /// Constrains a route value to a valid semantic version. + internal class VersionConstraint : RegexRouteConstraint + { + /********* + ** Public methods + *********/ + /// Construct an instance. + public VersionConstraint() + : base(@"^v(?>(?0|[1-9]\d*))\.(?>(?0|[1-9]\d*))(?>(?:\.(?0|[1-9]\d*))?)(?:-(?(?>[a-z0-9]+[\-\.]?)+))?$") { } + } +} diff --git a/src/StardewModdingAPI.Web/Startup.cs b/src/StardewModdingAPI.Web/Startup.cs index d5b828b7..eaf14983 100644 --- a/src/StardewModdingAPI.Web/Startup.cs +++ b/src/StardewModdingAPI.Web/Startup.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Rewrite; +using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -42,6 +43,7 @@ namespace StardewModdingAPI.Web { services .Configure(this.Configuration.GetSection("ModUpdateCheck")) + .Configure(options => options.ConstraintMap.Add("semanticVersion", typeof(VersionConstraint))) .AddMemoryCache() .AddMvc() .ConfigureApplicationPartManager(manager => manager.FeatureProviders.Add(new InternalControllerFeatureProvider())) @@ -62,22 +64,7 @@ namespace StardewModdingAPI.Web loggerFactory.AddDebug(); app .UseRewriter(new RewriteOptions().Add(new RewriteSubdomainRule())) // convert subdomain.smapi.io => smapi.io/subdomain for routing - .UseMvc(route => - { - route.MapRoute( - name: "API", - template: "api/{version}/{controller}/{action?}", - defaults: new - { - action = "GetAsync" - }, - constraints: new - { - // version regex from SMAPI's SemanticVersion implementation - version = @"^v(?>(?0|[1-9]\d*))\.(?>(?0|[1-9]\d*))(?>(?:\.(?0|[1-9]\d*))?)(?:-(?(?>[a-z0-9]+[\-\.]?)+))?$" - } - ); - }); + .UseMvc(); } } } diff --git a/src/StardewModdingAPI/Framework/WebApiClient.cs b/src/StardewModdingAPI/Framework/WebApiClient.cs index 0ee57648..8f0b403d 100644 --- a/src/StardewModdingAPI/Framework/WebApiClient.cs +++ b/src/StardewModdingAPI/Framework/WebApiClient.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Net; +using System.Text; using System.Threading.Tasks; using Newtonsoft.Json; using StardewModdingAPI.Models; @@ -40,8 +41,10 @@ namespace StardewModdingAPI.Framework /// The mod keys for which to fetch the latest version. public async Task> GetModInfoAsync(params string[] modKeys) { - string url = $"v{this.Version}/mods?modKeys={Uri.EscapeDataString(string.Join(",", modKeys))}"; - return await this.GetAsync>(url); + return await this.PostAsync>( + $"v{this.Version}/mods", + new ModSearchModel(modKeys) + ); } @@ -49,13 +52,27 @@ namespace StardewModdingAPI.Framework ** Private methods *********/ /// Fetch the response from the backend API. - /// The expected response type. + /// The body content type. + /// The expected response type. /// The request URL, optionally excluding the base URL. - private async Task GetAsync(string url) + /// The body content to post. + private async Task PostAsync(string url, TBody content) { - // build request (avoid HttpClient for Mac compatibility) + /*** + ** Note: avoid HttpClient for Mac compatibility. + ***/ + + // serialise content + byte[] data = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(content)); + + // build request HttpWebRequest request = WebRequest.CreateHttp(new Uri(this.BaseUrl, url).ToString()); + request.Method = "POST"; request.UserAgent = $"SMAPI/{this.Version}"; + request.ContentType = "application/json"; + request.ContentLength = data.Length; + using (Stream bodyStream = request.GetRequestStream()) + bodyStream.Write(data, 0, data.Length); // fetch data using (WebResponse response = await request.GetResponseAsync()) @@ -63,7 +80,7 @@ namespace StardewModdingAPI.Framework using (StreamReader reader = new StreamReader(responseStream)) { string responseText = reader.ReadToEnd(); - return JsonConvert.DeserializeObject(responseText); + return JsonConvert.DeserializeObject(responseText); } } } From d338322bc63387c151a5d75bf52e12c71d34ea07 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 25 Sep 2017 17:40:00 -0400 Subject: [PATCH 114/186] fix update checks not normalising remote versions (#336) --- src/StardewModdingAPI/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 7b3048c0..0c4648ab 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -577,7 +577,7 @@ namespace StardewModdingAPI // track update ISemanticVersion localVersion = new SemanticVersion(mod.DataRecord?.GetLocalVersionForUpdateChecks(mod.Manifest.Version.ToString())); - ISemanticVersion latestVersion = new SemanticVersion(mod.DataRecord?.GetRemoteVersionForUpdateChecks(info.Version)); + ISemanticVersion latestVersion = new SemanticVersion(mod.DataRecord?.GetRemoteVersionForUpdateChecks(new SemanticVersion(info.Version).ToString())); bool isUpdate = latestVersion.IsNewerThan(localVersion); this.VerboseLog($" {mod.DisplayName} ({result.Key}): {(isUpdate ? $"{mod.Manifest.Version}{(!localVersion.Equals(mod.Manifest.Version) ? $" [{localVersion}]" : "")} => {info.Version}{(!latestVersion.Equals(new SemanticVersion(info.Version)) ? $" [{latestVersion}]" : "")}" : "OK")}."); if (isUpdate) From a4dfcf229e26abb0848a49e61f30dc316edcaf11 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 25 Sep 2017 17:40:39 -0400 Subject: [PATCH 115/186] add mod data for update checks (#361) --- .../StardewModdingAPI.config.json | 908 ++++++++++++++++-- 1 file changed, 837 insertions(+), 71 deletions(-) diff --git a/src/StardewModdingAPI/StardewModdingAPI.config.json b/src/StardewModdingAPI/StardewModdingAPI.config.json index a768c762..2e1c3489 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.config.json +++ b/src/StardewModdingAPI/StardewModdingAPI.config.json @@ -77,6 +77,9 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "AlternativeUrl": "https://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.1": { "Status": "AssumeBroken" } // broke in SDV 1.1 + }, + "MapLocalVersions": { + "1.1-1078": "1.1" } }, { @@ -88,6 +91,11 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~0.1": { "Status": "AssumeBroken" } // broke in SMAPI 1.9 } }, + { + // Adjust Monster + "ID": "mmanlapat.AdjustMonster", + "Defaults": { "NexusID": 1161 } + }, { // Advanced Location Loader "ID": "Entoarox.AdvancedLocationLoader", @@ -96,6 +104,11 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~1.2.10": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } }, + { + // Adventure Shop Inventory + "ID": "HammurabiAdventureShopInventory", + "Defaults": { "ChucklefishID": 4608 } + }, { // AgingMod "ID": "skn.AgingMod", @@ -105,26 +118,41 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } }, + { + // All Crops All Seasons + "ID": "29ee8246-d67b-4242-a340-35a9ae0d5dd7 | community.AllCropsAllSeasons", // changed in 1.3 + "Defaults": { "NexusID": 170 } + }, + { + // All Professions + "ID": "8c37b1a7-4bfb-4916-9d8a-9533e6363ea3 | community.AllProfessions", // changed in 1.2 + "Defaults": { "NexusID": 174 } + }, { // Almighty Tool - "ID": "AlmightyTool.dll", + "ID": "AlmightyTool.dll | 439", // changed in 1.2.1 "Defaults": { "NexusID": 439 }, "Compatibility": { "~1.1.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 + }, + "MapRemoteVersions": { + "1.21": "1.2.1" } }, { // Animal Mood Fix "ID": "GPeters-AnimalMoodFix", "Compatibility": { - "~": { "Status": "Obsolete", "ReasonPhrase": "the animal mood bugs were fixed in Stardew Valley 1.2." } + "~": { + "Status": "Obsolete", + "ReasonPhrase": "the animal mood bugs were fixed in Stardew Valley 1.2." + } } }, { // Animal Sitter - "ID": "AnimalSitter.dll", + "ID": "AnimalSitter.dll | jwdred.AnimalSitter", // changed in 1.0.9 "Defaults": { "NexusID": 581 }, - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0.8": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -138,6 +166,77 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~1.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } }, + { + // Auto Animal Doors + "ID": "AaronTaggart.AutoAnimalDoors", + "Defaults": { "NexusID": 1019 }, + "MapRemoteVersions": { + "1.1.1": "1.1" // manifest not updated + } + }, + { + // Auto-Eat + "ID": "BALANCEMOD_AutoEat | Permamiss.AutoEat", // changed in 1.1.1 + "Defaults": { "NexusID": 643 } + }, + { + // AutoGate + "ID": "AutoGate", + "Defaults": { "NexusID": 820 } + }, + { + // Automate + "ID": "Pathoschild.Automate", + "Defaults": { "NexusID": 1063 } + }, + { + // Automated Doors + "ID": "1abcfa07-2cf4-4dc3-a6e9-6068b642112b | azah.automated-doors", // changed in 1.4.1 + "Defaults": { "GitHubProject": "azah/AutomatedDoors" }, + "MapLocalVersions": { + "1.4.1-1": "1.4.1" + } + }, + { + // AutoSpeed + "ID": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'AutoSpeed'} | Omegasis.AutoSpeed", // changed in 1.4; disambiguate from other Alpha_Omegasis mods + "Defaults": { "NexusID": 443 } + }, + { + // Basic Sprinkler Improved + "ID": "lrsk_sdvm_bsi.0117171308", + "Defaults": { "NexusID": 833 }, + "MapRemoteVersions": { + "1.0.2": "1.0.1-release" // manifest not updated + } + }, + { + // Better Quality More Seasons + "ID": "SB_BQMS", + "Defaults": { "NexusID": 935 } + }, + { + // Better Quarry + "ID": "BetterQuarry", + "Defaults": { "NexusID": 771 } + }, + { + // Better Ranching + "ID": "BetterRanching", + "Defaults": { "NexusID": 859 } + }, + { + // Better Shipping Box + "ID": "Kithio:BetterShippingBox", + "Defaults": { "ChucklefishID": 4302 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.0.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + }, + "MapLocalVersions": { + "1.0.1": "1.0.2" + } + }, { // Better Sprinklers "ID": "SPDSprinklersMod | Speeder.BetterSprinklers", // changed in 2.3 @@ -147,29 +246,37 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~2.3.1-pathoschild-update": { "Status": "AssumeBroken" } // broke in SDV 1.2 } }, + { + // Billboard Anywhere + "ID": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Billboard Anywhere'} | Omegasis.BillboardAnywhere", // changed in 1.4; disambiguate from other mods by Alpha_Omegasis + "Defaults": { "NexusID": 492 } + }, { // Birthday Mail - "ID": "005e02dc-d900-425c-9c68-1ff55c5a295d", + "ID": "005e02dc-d900-425c-9c68-1ff55c5a295d | KathrynHazuka.BirthdayMail", // changed in 1.2.3-pathoschild-update "Defaults": { "NexusID": 276 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.2.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 } }, + { + // Breed Like Rabbits + "ID": "dycedarger.breedlikerabbits", + "Defaults": { "NexusID": 948 } + }, { // Build Endurance - "ID": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'BuildEndurance'}", // disambiguate from other Alpha_Omegasis mods + "ID": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'BuildEndurance'} | Omegasis.BuildEndurance", // changed in 1.4; disambiguate from other Alpha_Omegasis mods "Defaults": { "NexusID": 445 }, - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } }, { // Build Health - "ID": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'BuildHealth'}", // disambiguate from other Alpha_Omegasis mods + "ID": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'BuildHealth'} | Omegasis.BuildHealth", // changed in 1.4; disambiguate from other Alpha_Omegasis mods "Defaults": { "NexusID": 446 }, - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -185,16 +292,41 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, { // Buy Back Collectables - "ID": "BuyBackCollectables", + "ID": "BuyBackCollectables | Omegasis.BuyBackCollectables", // changed in 1.4 "Defaults": { "NexusID": 507 }, - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } }, + { + // Carry Chest + "ID": "spacechase0.CarryChest", + "Defaults": { "NexusID": 1333 } + }, + { + // Casks Anywhere + "ID": "CasksAnywhere", + "Defaults": { "NexusID": 878 }, + "MapLocalVersions": { + "1.1-alpha": "1.1" + } + }, + { + // Categorize Chests + "ID": "CategorizeChests", + "Defaults": { "NexusID": 1300 } + }, + { + // ChefsCloset + "ID": "Duder.ChefsCloset", + "Defaults": { "NexusID": 1030 }, + "MapLocalVersions": { + "1.3-1": "1.3" + } + }, { // Chest Label System - "ID": "SPDChestLabel", + "ID": "SPDChestLabel | Speeder.ChestLabel", // changed in 1.5.1-pathoschild-update "Defaults": { "NexusID": 242 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { @@ -203,7 +335,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, { // Chest Pooling - "ID": "ChestPooling.dll", + "ID": "ChestPooling.dll | mralbobo.ChestPooling", // changed in 1.3 "Defaults": { "GitHubProject": "mralbobo/stardew-chest-pooling" }, "Compatibility": { "~1.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 @@ -237,7 +369,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, { // CJB Cheats Menu - "ID": "CJBCheatsMenu", + "ID": "CJBCheatsMenu | CJBok.CheatsMenu", // changed in 1.14 "Defaults": { "NexusID": 4 }, "Compatibility": { "~1.12": { "Status": "AssumeBroken" } // broke in SDV 1.1 @@ -245,7 +377,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, { // CJB Item Spawner - "ID": "CJBItemSpawner", + "ID": "CJBItemSpawner | CJBok.ItemSpawner", // changed in 1.7 "Defaults": { "NexusID": 93 }, "Compatibility": { "~1.5": { "Status": "AssumeBroken" } // broke in SDV 1.1 @@ -253,12 +385,17 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, { // CJB Show Item Sell Price - "ID": "CJBShowItemSellPrice", + "ID": "CJBShowItemSellPrice | CJBok.ShowItemSellPrice", // changed in 1.7 "Defaults": { "NexusID": 5 }, "Compatibility": { "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.2 } }, + { + // Clean Farm + "ID": "tstaples.CleanFarm", + "Defaults": { "NexusID": 794 } + }, { // Climates of Ferngill "ID": "KoihimeNakamura.ClimatesOfFerngill", @@ -280,7 +417,10 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha // Colored Chests "ID": "4befde5c-731c-4853-8e4b-c5cdf946805f", "Compatibility": { - "~": { "Status": "Obsolete", "ReasonPhrase": "colored chests were added in Stardew Valley 1.1." } + "~": { + "Status": "Obsolete", + "ReasonPhrase": "colored chests were added in Stardew Valley 1.1." + } } }, { @@ -292,6 +432,19 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } }, + { + // Community Bundle Item Tooltip + "ID": "musbah.bundleTooltip", + "Defaults": { "NexusID": 1329 } + }, + { + // Configurable Machines + "ID": "21da6619-dc03-4660-9794-8e5b498f5b97", + "Defaults": { "NexusID": 280 }, + "MapLocalVersions": { + "1.2-beta": "1.2" + } + }, { // Configurable Shipping Dates "ID": "ConfigurableShippingDates", @@ -311,21 +464,58 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, { // CrabNet - "ID": "CrabNet.dll", + "ID": "CrabNet.dll | jwdred.CrabNet", // changed in 1.0.5 "Defaults": { "NexusID": 584 }, - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } }, + { + // Current Location + "ID": "CurrentLocation102120161203", + "Defaults": { "NexusID": 638 } + }, + { + // Custom Critters + "ID": "spacechase0.CustomCritters", + "Defaults": { "NexusID": 1255 } + }, + { + // Custom Element Handler + "ID": "Platonymous.CustomElementHandler", + "Defaults": { "NexusID": 1068 } + }, + { + // Custom Farming + "ID": "Platonymous.CustomFarming", + "Defaults": { "NexusID": 991 } + }, + { + // Custom Farm Types + "ID": "spacechase0.CustomFarmTypes", + "Defaults": { "NexusID": 1140 } + }, + { + // Custom Furniture + "ID": "Platonymous.CustomFurniture", + "Defaults": { "NexusID": 1254 } + }, { // Customize Exterior - "ID": "CustomizeExterior", + "ID": "CustomizeExterior | spacechase0.CustomizeExterior", // changed in 1.0.3 "Defaults": { "NexusID": 1099 }, "Compatibility": { "~1.0.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } }, + { + // Customizable Cart Redux + "ID": "KoihimeNakamura.CCR", + "Defaults": { "NexusID": 1402 }, + "MapLocalVersions": { + "1.1-20170917": "1.1" + } + }, { // Customizable Traveling Cart Days "ID": "TravelingCartYyeahdude", @@ -335,6 +525,29 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } }, + { + // Custom Linens + "ID": "Mevima.CustomLinens", + "Defaults": { "NexusID": 1027 }, + "MapRemoteVersions": { + "1.1": "1.0" // manifest not updated + } + }, + { + // Custom Shops Redux + "ID": "Omegasis.CustomShopReduxGui", + "Defaults": { "NexusID": 1378 } + }, + { + // Custom TV + "ID": "Platonymous.CustomTV", + "Defaults": { "NexusID": 1139 } + }, + { + // Daily Luck Message + "ID": "Schematix.DailyLuckMessage", + "Defaults": { "NexusID": 1327 } + }, { // Daily News "ID": "bashNinja.DailyNews", @@ -343,6 +556,16 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } }, + { + // Daily Quest Anywhere + "ID": "DailyQuest | Omegasis.DailyQuestAnywhere", // changed in 1.4 + "Defaults": { "NexusID": 513 } + }, + { + // Debug Mode + "ID": "Pathoschild.Stardew.DebugMode | Pathoschild.DebugMode", // changed in 1.4 + "Defaults": { "NexusID": 679 } + }, { // Dynamic Checklist "ID": "gunnargolf.DynamicChecklist", @@ -352,15 +575,31 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } }, + { + // Dynamic Horses + "ID": "Bpendragon-DynamicHorses", + "Defaults": { "NexusID": 874 }, + "MapRemoteVersions": { + "1.2": "1.1-release" // manifest not updated + } + }, { // Dynamic Machines "ID": "DynamicMachines", "Defaults": { "NexusID": 374 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { - "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + "~1.1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + }, + "MapLocalVersions": { + "1.1": "1.1.1" } }, + { + // Dynamic NPC Sprites + "ID": "BashNinja.DynamicNPCSprites", + "Defaults": { "NexusID": 1183 } + }, { // Empty Hands "ID": "QuicksilverFox.EmptyHands", @@ -372,7 +611,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, { // Enemy Health Bars - "ID": "SPDHealthBar", + "ID": "SPDHealthBar | Speeder.HealthBars", // changed in 1.7.1-pathoschild-update "Defaults": { "NexusID": 193 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { @@ -387,9 +626,24 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~1.7.9": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } }, + { + // Expanded Fridge / Dynamic Expanded Fridge + "ID": "Uwazouri.ExpandedFridge", + "Defaults": { "NexusID": 1191 } + }, + { + // Experience Bars + "ID": "ExperienceBars | spacechase0.ExperienceBars", // changed in 1.0.2 + "Defaults": { "NexusID": 509 } + }, + { + // Extended Bus System + "ID": "ExtendedBusSystem", + "Defaults": { "ChucklefishID": 4373 } + }, { // Extended Fridge - "ID": "Mystra007ExtendedFridge", + "ID": "Mystra007ExtendedFridge | Crystalmir.ExtendedFridge", // changed in 1.0.1 "Defaults": { "NexusID": 485 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { @@ -405,9 +659,14 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~1.0.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 } }, + { + // Extended Minecart + "ID": "{ID:'EntoaroxFurnitureAnywhere', Name:'Extended Minecart'} | Entoarox.ExtendedMinecart", // changed in 1.6.1 + "Defaults": { "ChucklefishID": 4359 } + }, { // Fall 28 Snow Day - "ID": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Fall28 Snow Day'}", // disambiguate from other mods by Alpha_Omegasis + "ID": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Fall28 Snow Day'} | Omegasis.Fall28SnowDay", // changed in 1.4; disambiguate from other mods by Alpha_Omegasis "Defaults": { "NexusID": 486 }, "Compatibility": { "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -455,6 +714,16 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~1.0.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } }, + { + // Fast Animations + "ID": "Pathoschild.FastAnimations", + "Defaults": { "NexusID": 1089 } + }, + { + // Faster Paths + "ID": "{ID:'821ce8f6-e629-41ad-9fde-03b54f68b0b6', Name:'Faster Paths'} | 615f85f8-5c89-44ee-aecc-c328f172e413 | Entoarox.FasterPaths", // changed in 1.2 and 1.3; disambiguate from Shop Expander + "Defaults": { "ChucklefishID": 3641 } + }, { // Faster Run "ID": "FasterRun.dll", @@ -464,6 +733,16 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } }, + { + // Fishing Adjust + "ID": "shuaiz.FishingAdjustMod", + "Defaults": { "NexusID": 1350 } + }, + { + // Fishing Tuner Redux + "ID": "HammurabiFishingTunerRedux", + "Defaults": { "ChucklefishID": 4578 } + }, { // FlorenceMod "ID": "FlorenceMod.dll", @@ -471,8 +750,16 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + }, + "MapLocalVersions": { + "1.0.1": "1.1" } }, + { + // Flower Color Picker + "ID": "spacechase0.FlowerColorPicker", + "Defaults": { "NexusID": 1229 } + }, { // Forage at the Farm "ID": "ForageAtTheFarm", @@ -483,12 +770,14 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - // Instant Geode - "ID": "InstantGeode", - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", - "Compatibility": { - "~1.12": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } + // Furniture Anywhere + "ID": "{ID:'EntoaroxFurnitureAnywhere', Name:'Furniture Anywhere'} | Entoarox.FurnitureAnywhere", // changed in 1.1; disambiguate from Extended Minecart + "Defaults": { "ChucklefishID": 4324 } + }, + { + // Game Reminder + "ID": "mmanlapat.GameReminder", + "Defaults": { "NexusID": 1153 } }, { // Gate Opener @@ -515,14 +804,24 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~3.3": { "Status": "AssumeBroken" } // broke in SDV 1.2 } }, + { + // Giant Crop Ring + "ID": "cat.giantcropring", + "Defaults": { "NexusID": 1182 } + }, { // Gift Taste Helper - "ID": "8008db57-fa67-4730-978e-34b37ef191d6", + "ID": "8008db57-fa67-4730-978e-34b37ef191d6 | tstaples.GiftTasteHelper", // changed in 2.5 "Defaults": { "NexusID": 229 }, "Compatibility": { "~2.3.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 } }, + { + // Grandfather's Gift + "ID": "ShadowDragon.GrandfathersGift", + "Defaults": { "NexusID": 985 } + }, { // Happy Animals "ID": "HappyAnimals", @@ -532,13 +831,28 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - // Happy Birthday - "ID": "{ID:'HappyBirthday', Author:'Alpha_Omegasis'}", // disambiguate from Oxyligen's fork + // Happy Birthday (Omegasis) + "ID": "{ID:'HappyBirthday', Author:'Alpha_Omegasis'} | Omegasis.HappyBirthday", // changed in 1.4; disambiguate from Oxyligen's fork "Defaults": { "NexusID": 520 }, "Compatibility": { "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } }, + { + // Happy Birthday (Oxyligen fork) + "ID": "{ID:'HappyBirthday', Author:'Alpha_Omegasis/Oxyligen'}", // disambiguate from Oxyligen's fork + "Defaults": { "NexusID": 1064 } + }, + { + // Harp of Yoba Redux + "ID": "Platonymous.HarpOfYobaRedux", + "Defaults": { "NexusID": 914 } + }, + { + // Harvest Moon Witch Princess + "ID": "Sasara.WitchPrincess", + "Defaults": { "NexusID": 1157 } + }, { // Harvest With Scythe "ID": "965169fd-e1ed-47d0-9f12-b104535fb4bc", @@ -549,7 +863,17 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - // Hunger for Food + // Horse Whistle (icepuente) + "ID": "icepuente.HorseWhistle", + "Defaults": { "NexusID": 1131 } + }, + { + // Hunger (Yyeadude) + "ID": "HungerYyeadude", + "Defaults": { "NexusID": 613 } + }, + { + // Hunger for Food (Tigerle) "ID": "HungerForFoodByTigerle", "Defaults": { "NexusID": 810 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", @@ -557,6 +881,22 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~0.1.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } }, + { + // Hunger Mod (skn) + "ID": "skn.HungerMod", + "Defaults": { "NexusID": 1127 }, + "MapRemoteVersions": { + "1.2.1": "1.0" // manifest not updated + } + }, + { + // Idle Pause + "ID": "Veleek.IdlePause", + "Defaults": { "NexusID": 1092 }, + "MapRemoteVersions": { + "1.2": "1.1" // manifest not updated + } + }, { // Improved Quality of Life "ID": "Demiacle.ImprovedQualityOfLife", @@ -566,6 +906,44 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } }, + { + // Instant Geode + "ID": "InstantGeode", + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.12": { "Status": "AssumeBroken" } // broke in SDV 1.2 + } + }, + { + // Instant Grow Trees + "ID": "dc50c58b-c7d8-4e60-86cc-e27b5d95ee59 | community.InstantGrowTrees", // changed in 1.2 + "Defaults": { "NexusID": 173 } + }, + { + // Interaction Helper + "ID": "HammurabiInteractionHelper", + "Defaults": { "ChucklefishID": 4640 }, + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.0.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Item Auto Stacker + "ID": "cat.autostacker", + "Defaults": { "NexusID": 1184 }, + "MapRemoteVersions": { + "1.0.1": "1.0" // manifest not updated + } + }, + { + // Junimo Farm + "ID": "Platonymous.JunimoFarm", + "Defaults": { "NexusID": 984 }, + "MapRemoteVersions": { + "1.1.2": "1.1.1" // manifest not updated + } + }, { // Less Strict Over-Exertion (AntiExhaustion) "ID": "BALANCEMOD_AntiExhaustion", @@ -573,8 +951,26 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + }, + "MapLocalVersions": { + "0.0": "1.1" } }, + { + // Level Up Notifications + "ID": "Level Up Notifications", + "Defaults": { "NexusID": 855 } + }, + { + // Location and Music Logging + "ID": "Brandy Lover.LMlog", + "Defaults": { "NexusID": 1366 } + }, + { + // Longevity + "ID": "RTGOAT.Longevity", + "Defaults": { "NexusID": 649 } + }, { // Lookup Anything "ID": "LookupAnything | Pathoschild.LookupAnything", // changed in 1.10.1 @@ -583,6 +979,11 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~1.10.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 } }, + { + // Love Bubbles + "ID": "LoveBubbles", + "Defaults": { "NexusID": 1318 } + }, { // Loved Labels "ID": "LovedLabels.dll", @@ -601,7 +1002,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, { // MailOrderPigs - "ID": "MailOrderPigs.dll", + "ID": "MailOrderPigs.dll | jwdred.MailOrderPigs", // changed in 1.0.2 "Defaults": { "NexusID": 632 }, "Compatibility": { "~1.0.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -615,6 +1016,11 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~0.3.6": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } }, + { + // Map Image Exporter + "ID": "MapImageExporter | spacechase0.MapImageExporter", // changed in 1.0.2 + "Defaults": { "NexusID": 1073 } + }, { // Message Box [API]? (ChatMod) "ID": "Kithio:ChatMod", @@ -624,11 +1030,24 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } }, + { + // Mining at the Farm + "ID": "MiningAtTheFarm", + "Defaults": { "NexusID": 674 } + }, + { + // Mining With Explosives + "ID": "MiningWithExplosives", + "Defaults": { "NexusID": 770 } + }, { // Modder Serialization Utility "ID": "SerializerUtils-0-1", "Compatibility": { - "~": { "Status": "Obsolete", "ReasonPhrase": "it's no longer maintained or used." } + "~": { + "Status": "Obsolete", + "ReasonPhrase": "it's no longer maintained or used." + } } }, { @@ -640,6 +1059,11 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~1.0.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } }, + { + // More Map Layers + "ID": "Platonymous.MoreMapLayers", + "Defaults": { "NexusID": 1134 } + }, { // More Pets "ID": "821ce8f6-e629-41ad-9fde-03b54f68b0b6MOREPETS | Entoarox.MorePets", // changed in 1.3 @@ -650,13 +1074,23 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, { // More Rain - "ID": "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'More_Rain'}", // disambiguate from other mods by Alpha_Omegasis + "ID": "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'More_Rain'} | Omegasis.MoreRain", // changed in 1.5; disambiguate from other mods by Alpha_Omegasis "Defaults": { "NexusID": 441 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } }, + { + // More Weapons + "ID": "Joco80.MoreWeapons", + "Defaults": { "NexusID": 1168 } + }, + { + // Move Faster + "ID": "shuaiz.MoveFasterMod", + "Defaults": { "NexusID": 1351 } + }, { // Multiple Sprites and Portraits On Rotation (File Loading) "ID": "FileLoading", @@ -664,11 +1098,14 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.12": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + }, + "MapLocalVersions": { + "1.1": "1.12" } }, { // Museum Rearranger - "ID": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Museum Rearranger'}", // disambiguate from other mods by Alpha_Omegasis + "ID": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Museum Rearranger'} | Omegasis.MuseumRearranger", // changed in 1.4; disambiguate from other mods by Alpha_Omegasis "Defaults": { "NexusID": 428 }, "Compatibility": { "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -686,18 +1123,34 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Night Owl "Defaults": { "NexusID": 433 }, - "ID": "{ID:'SaveAnywhere', Name:'Stardew_NightOwl'}", // disambiguate from Save Anywhere + "ID": "{ID:'SaveAnywhere', Name:'Stardew_NightOwl'} | Omegasis.NightOwl", // changed in 1.4; disambiguate from Save Anywhere "Compatibility": { - "~2.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + }, + "MapLocalVersions": { + "2.1": "1.3" // 1.3 had wrong version in manifest } }, { // No Debug Mode "ID": "NoDebugMode", "Compatibility": { - "~": { "Status": "Obsolete", "ReasonPhrase": "debug mode was removed in SMAPI 1.0." } + "~": { + "Status": "Obsolete", + "ReasonPhrase": "debug mode was removed in SMAPI 1.0." + } } }, + { + // No Fence Decay + "ID": "cat.nofencedecay", + "Defaults": { "NexusID": 1180 } + }, + { + // No More Pets + "ID": "NoMorePets | Omegasis.NoMorePets", // changed in 1.4 + "Defaults": { "NexusID": 506 } + }, { // NoSoilDecay "ID": "289dee03-5f38-4d8e-8ffc-e440198e8610", @@ -707,12 +1160,20 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~0.5": { "Status": "AssumeBroken" } // broke in SDV 1.2, and uses Assembly.GetExecutingAssembly().Location } }, + { + // No Soil Decay Redux + "ID": "Platonymous.NoSoilDecayRedux", + "Defaults": { "NexusID": 1084 } + }, { // NPC Map Locations "ID": "NPCMapLocationsMod", "Defaults": { "NexusID": 239 }, "Compatibility": { - "1.42~1.43": { "Status": "AssumeBroken", "ReasonPhrase": "this version has an update check error which crashes the game." } + "1.42~1.43": { + "Status": "AssumeBroken", + "ReasonPhrase": "this version has an update check error which crashes the game." + } } }, { @@ -724,6 +1185,11 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } }, + { + // Object Time Left + "ID": "spacechase0.ObjectTimeLeft", + "Defaults": { "NexusID": 1315 } + }, { // OmniFarm "ID": "BlueMod_OmniFarm", @@ -743,10 +1209,13 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, { // PelicanFiber - "ID": "PelicanFiber.dll", + "ID": "PelicanFiber.dll | jwdred.PelicanFiber", // changed in 3.0.1 "Defaults": { "NexusID": 631 }, "Compatibility": { "~3.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + }, + "MapRemoteVersions": { + "3.0.2": "3.0.1" // didn't change manifest version } }, { @@ -767,17 +1236,32 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.1 } }, + { + // Plant on Grass + "ID": "Demiacle.PlantOnGrass", + "Defaults": { "NexusID": 1026 } + }, { // Point-and-Plant - "ID": "PointAndPlant.dll", + "ID": "PointAndPlant.dll | jwdred.PointAndPlant", // changed in 1.0.3 "Defaults": { "NexusID": 572 }, "Compatibility": { "~1.0.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 } }, { - // PrairieKingMadeEasy - "ID": "PrairieKingMadeEasy.dll", + // Pony Weight Loss Program + "ID": "BadNetCode.PonyWeightLossProgram", + "Defaults": { "NexusID": 1232 } + }, + { + // Portraiture + "ID": "Platonymous.Portraiture", + "Defaults": { "NexusID": 999 } + }, + { + // Prairie King Made Easy + "ID": "PrairieKingMadeEasy.dll | Mucchan.PrairieKingMadeEasy", // changed in 1.0.1 "Defaults": { "ChucklefishID": 3594 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { @@ -785,13 +1269,33 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - // RainRandomizer + // Quest Delay + "ID": "BadNetCode.QuestDelay", + "Defaults": { "NexusID": 1239 } + }, + { + // Rain Randomizer "ID": "RainRandomizer.dll", "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } }, + { + // Recatch Legendary Fish + "ID": "b3af8c31-48f0-43cf-8343-3eb08bcfa1f9 | community.RecatchLegendaryFish", // changed in 1.3 + "Defaults": { "NexusID": 172 } + }, + { + // Regeneration + "ID": "HammurabiRegeneration", + "Defaults": { "ChucklefishID": 4584 } + }, + { + // Relationship Bar UI + "ID": "RelationshipBar", + "Defaults": { "NexusID": 1009 } + }, { // RelationshipsEnhanced "ID": "relationshipsenhanced", @@ -802,23 +1306,44 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - // RelationShipStatus + // Relationship Status "ID": "relationshipstatus", "Defaults": { "NexusID": 751 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0.5": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + }, + "MapRemoteVersions": { + "1.0.5": "1.0.4" // not updated in manifest } }, + { + // Rented Tools + "ID": "JarvieK.RentedTools", + "Defaults": { "NexusID": 1307 } + }, { // Replanter - "ID": "Replanter.dll", + "ID": "Replanter.dll | jwdred.Replanter", // changed in 1.0.5 "Defaults": { "NexusID": 589 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } }, + { + // ReRegeneration + "ID": "lrsk_sdvm_rerg.0925160827", + "Defaults": { "ChucklefishID": 4465 }, + "MapLocalVersions": { + "1.1.2-release": "1.1.2" + } + }, + { + // Reseed + "ID": "Roc.Reseed", + "Defaults": { "NexusID": 887 } + }, { // Reusable Wallpapers and Floors (Wallpaper Retain) "ID": "dae1b553-2e39-43e7-8400-c7c5c836134b", @@ -828,6 +1353,21 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~1.5": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } }, + { + // Ring of Fire + "ID": "Platonymous.RingOfFire", + "Defaults": { "NexusID": 1166 } + }, + { + // Rope Bridge + "ID": "RopeBridge", + "Defaults": { "NexusID": 824 } + }, + { + // Rotate Toolbar + "ID": "Pathoschild.RotateToolbar", + "Defaults": { "NexusID": 1100 } + }, { // Rush Orders "ID": "RushOrders | spacechase0.RushOrders", // changed in 1.1 @@ -838,13 +1378,34 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, { // Save Anywhere - "ID": "{ID:'SaveAnywhere', Name:'Save Anywhere'}", // disambiguate from Night Owl + "ID": "{ID:'SaveAnywhere', Name:'Save Anywhere'} | Omegasis.SaveAnywhere", // changed in 2.5; disambiguate from Night Owl "Defaults": { "NexusID": 444 }, "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~2.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + }, + "MapRemoteVersions": { + "2.6": "2.5" // not updated in manifest } }, + { + // Save Backup + "ID": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'Stardew_Save_Backup'} | Omegasis.SaveBackup", // changed in 1.3; disambiguate from other Alpha_Omegasis mods + "Defaults": { "NexusID": 435 }, + "Compatibility": { + "~1.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } + }, + { + // Scroll to Blank + "ID": "caraxian.scroll.to.blank", + "Defaults": { "ChucklefishID": 4405 } + }, + { + // Scythe Harvesting + "ID": "ScytheHarvesting | mmanlapat.ScytheHarvesting", // changed in 1.6 + "Defaults": { "NexusID": 1106 } + }, { // Seasonal Immersion "ID": "EntoaroxSeasonalHouse | EntoaroxSeasonalBuildings | EntoaroxSeasonalImmersion | Entoarox.SeasonalImmersion", // changed in 1.1, 1.6 or earlier, and 1.7 @@ -854,6 +1415,19 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~1.8.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } }, + { + // Seed Bag + "ID": "Platonymous.SeedBag", + "Defaults": { "NexusID": 1133 } + }, + { + // Self Service + "ID": "JarvieK.SelfService", + "Defaults": { "NexusID": 1304 }, + "MapRemoteVersions": { + "0.2.1": "0.2" // manifest not updated + } + }, { // Send Items "ID": "Denifia.SendItems", @@ -879,8 +1453,16 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + }, + "MapRemoteVersions": { + "1.1.1": "1.1" // not updated in manifest } }, + { + // Ship Anywhere + "ID": "spacechase0.ShipAnywhere", + "Defaults": { "NexusID": 1379 } + }, { // Shipment Tracker "ID": "7e474181-e1a0-40f9-9c11-d08a3dcefaf3", @@ -892,9 +1474,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, { // Shop Expander - "ID": "821ce8f6-e629-41ad-9fde-03b54f68b0b6 | EntoaroxShopExpander | Entoarox.ShopExpander", // changed in 1.5 and 1.5.2 + "ID": "{ID:'821ce8f6-e629-41ad-9fde-03b54f68b0b6', Name:'Shop Expander'} | EntoaroxShopExpander | Entoarox.ShopExpander", // changed in 1.5 and 1.5.2; disambiguate from Faster Paths "Defaults": { "ChucklefishID": 4381 }, - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.5.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -906,13 +1487,30 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~0.9": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + }, + "MapLocalVersions": { + "0.9-500": "0.9" } }, + { + // Shroom Spotter + "ID": "TehPers.ShroomSpotter", + "Defaults": { "NexusID": 908 } + }, + { + // Simple Crop Label + "ID": "SimpleCropLabel", + "Defaults": { "NexusID": 314 } + }, + { + // Simple Sound Manager + "ID": "Omegasis.SimpleSoundManager", + "Defaults": { "NexusID": 1410 } + }, { // Simple Sprinklers - "ID": "SimpleSprinkler.dll", + "ID": "SimpleSprinkler.dll | tZed.SimpleSprinkler", // changed in 1.5 "Defaults": { "NexusID": 76 }, - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.4": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -924,11 +1522,14 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.2.2": { "Status": "AssumeBroken" } // broke in SMAPI 1.9 (has multiple Mod instances) + }, + "MapLocalVersions": { + "0.0": "1.4" } }, { // Skill Prestige - "ID": "6b843e60-c8fc-4a25-a67b-4a38ac8dcf9b", + "ID": "6b843e60-c8fc-4a25-a67b-4a38ac8dcf9b | alphablackwolf.skillPrestige", // changed circa 1.2.3 "Defaults": { "NexusID": 569 }, "Compatibility": { "~1.0.9": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -936,12 +1537,35 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, { // Skill Prestige: Cooking Adapter - "ID": "20d6b8a3-b6e7-460b-a6e4-07c2b0cb6c63", + "ID": "20d6b8a3-b6e7-460b-a6e4-07c2b0cb6c63 | Alphablackwolf.CookingSkillPrestigeAdapter", // changed circa 1.1 "Defaults": { "NexusID": 569 }, "Compatibility": { "~1.0.9": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + }, + "MapRemoteVersions": { + "1.2.3": "1.1" // manifest not updated } }, + { + // Skip Intro + "ID": "SkipIntro | Pathoschild.SkipIntro", // changed in 1.4 + "Defaults": { "NexusID": 533 } + }, + { + // Skull Cavern Elevator + "ID": "SkullCavernElevator", + "Defaults": { "NexusID": 963 } + }, + { + // Skull Cave Saver + "ID": "8ac06349-26f7-4394-806c-95d48fd35774 | community.SkullCaveSaver", // changed in 1.1 + "Defaults": { "NexusID": 175 } + }, + { + // Sleepy Eye + "ID": "spacechase0.SleepyEye", + "Defaults": { "NexusID": 1152 } + }, { // Slower Fence Decay "ID": "SPDSlowFenceDecay", @@ -965,6 +1589,27 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Defaults": { "NexusID": 897 }, "Compatibility": { "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + }, + "MapLocalVersions": { + "1.3-20170917": "1.3" + } + }, + { + // SpaceCore + "ID": "spacechase0.SpaceCore", + "Defaults": { "NexusID": 1348 } + }, + { + // Speedster + "ID": "Platonymous.Speedster", + "Defaults": { "NexusID": 1102 } + }, + { + // Sprinkler Range + "ID": "cat.sprinklerrange", + "Defaults": { "NexusID": 1179 }, + "MapRemoteVersions": { + "1.0.1": "1.0" // manifest not updated } }, { @@ -987,11 +1632,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, { // Sprint and Dash Redux - "ID": "SPDSprintAndDash", - "Defaults": { "ChucklefishID": 4201 }, - "Compatibility": { - "~1.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 - } + "ID": "lrsk_sdvm_sndr.0921161059 | littleraskol.SprintAndDashRedux", // changed in 1.3 + "Defaults": { "ChucklefishID": 4201 } }, { // Sprinting Mod @@ -1000,11 +1642,14 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~2.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 + }, + "MapLocalVersions": { + "1.0": "2.1" // not updated in manifest } }, { // StackSplitX - "ID": "StackSplitX.dll", + "ID": "StackSplitX.dll | tstaples.StackSplitX", // changed circa 1.3.1 "Defaults": { "NexusID": 798 }, "Compatibility": { "~1.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 @@ -1019,13 +1664,23 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha } }, { - // Stardew Auto Backup - "ID": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'Stardew_Save_Backup'}", // disambiguate from other Alpha_Omegasis mods - "Defaults": { "NexusID": 435 }, + // Stardew Config Menu + "ID": "Juice805.StardewConfigMenu", + "Defaults": { "NexusID": 1312 } + }, + { + // Stardew Content Compatibility Layer (SCCL) + "ID": "SCCL", + "Defaults": { "NexusID": 889 }, "Compatibility": { - "~1.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + "~0.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 } }, + { + // Stardew Editor Game Integration + "ID": "spacechase0.StardewEditor.GameIntegration", + "Defaults": { "NexusID": 1298 } + }, { // Stardew Notification "ID": "stardewnotification", @@ -1037,7 +1692,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, { // Stardew Symphony - "ID": "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'Stardew_Symphony'}", // disambiguate other mods by Alpha_Omegasis + "ID": "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'Stardew_Symphony'} | Omegasis.StardewSymphony", // changed in 1.4; disambiguate other mods by Alpha_Omegasis "Defaults": { "NexusID": 425 }, "Compatibility": { "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -1047,9 +1702,17 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha // StarDustCore "ID": "StarDustCore", "Compatibility": { - "~": { "Status": "Obsolete", "ReasonPhrase": "it was only used by earlier versions of Save Anywhere, and is no longer used or maintained." } + "~": { + "Status": "Obsolete", + "ReasonPhrase": "it was only used by earlier versions of Save Anywhere, and is no longer used or maintained." + } } }, + { + // Starting Money + "ID": "StartingMoney | mmanlapat.StartingMoney", // changed in 1.1 + "Defaults": { "NexusID": 1138 } + }, { // StashItemsToChest "ID": "BlueMod_StashItemsToChest", @@ -1059,6 +1722,14 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~1.0.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } }, + { + // Stephan's Lots of Crops + "ID": "stephansstardewcrops", + "Defaults": { "ChucklefishID": 4314 }, + "MapRemoteVersions": { + "1.41": "1.1" // manifest not updated + } + }, { // Stone Bridge Over Pond (PondWithBridge) "ID": "PondWithBridge.dll", @@ -1066,8 +1737,16 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + }, + "MapLocalVersions": { + "0.0": "1.0" } }, + { + // Stumps to Hardwood Stumps + "ID": "StumpsToHardwoodStumps", + "Defaults": { "NexusID": 691 } + }, { // Super Greenhouse Warp Modifier "ID": "SuperGreenhouse", @@ -1077,6 +1756,11 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } }, + { + // Swim Almost Anywhere / Swim Suit + "ID": "Platonymous.SwimSuit", + "Defaults": { "NexusID": 1215 } + }, { // Tainted Cellar "ID": "TaintedCellar.dll", @@ -1085,6 +1769,16 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.1 or 1.11 } }, + { + // Tapper Ready + "ID": "skunkkk.TapperReady", + "Defaults": { "NexusID": 1219 } + }, + { + // Teh's Fishing Overhaul + "ID": "TehPers.FishingOverhaul", + "Defaults": { "NexusID": 866 } + }, { // Teleporter "ID": "Teleporter", @@ -1103,6 +1797,19 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~1.0.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 } }, + { + // TimeFreeze + "ID": "4108e859-333c-4fec-a1a7-d2e18c1019fe", + "Defaults": { "NexusID": 973 } + }, + { + // Time Reminder + "ID": "KoihimeNakamura.TimeReminder", + "Defaults": { "NexusID": 1000 }, + "MapLocalVersions": { + "1.0-20170314": "1.0.2" + } + }, { // TimeSpeed "ID": "TimeSpeed.dll | {ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'TimeSpeed'} | {ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'TimeSpeed Mod (unofficial)'} | community.TimeSpeed", // changed in 2.0.3 and 2.1; disambiguate other mods by Alpha_Omegasis @@ -1111,6 +1818,21 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~2.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 } }, + { + // TractorMod + "ID": "BlueMod_TractorMod | PhthaloBlue.TractorMod | community.TractorMod | Pathoschild.TractorMod", // changed in 3.2, 4.0 beta, and 4.0 + "Defaults": { "NexusID": 1401 } + }, + { + // Tree Transplant + "ID": "TreeTransplant", + "Defaults": { "NexusID": 1342 } + }, + { + // UI Info Suite + "ID": "Cdaragorn.UiInfoSuite", + "Defaults": { "NexusID": 1150 } + }, { // UiModSuite "ID": "Demiacle.UiModSuite", @@ -1118,8 +1840,21 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.2 + }, + "MapLocalVersions": { + "0.5": "1.0" // not updated in manifest } }, + { + // Variable Grass + "ID": "dantheman999.VariableGrass", + "Defaults": { "GitHubProject": "dantheman999301/StardewMods" } + }, + { + // Vertical Toolbar + "ID": "SB_VerticalToolMenu", + "Defaults": { "NexusID": 943 } + }, { // WakeUp "ID": "WakeUp.dll", @@ -1137,6 +1872,11 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } }, + { + // WarpAnimals + "ID": "Symen.WarpAnimals", + "Defaults": { "NexusID": 1400 } + }, { // Weather Controller "ID": "WeatherController.dll", @@ -1145,6 +1885,16 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~1.0.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 } }, + { + // What Farm Cave / WhatAMush + "ID": "WhatAMush", + "Defaults": { "NexusID": 1097 } + }, + { + // WHats Up + "ID": "wHatsUp", + "Defaults": { "NexusID": 1082 } + }, { // Wonderful Farm Life "ID": "WonderfulFarmLife.dll", @@ -1157,7 +1907,10 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha // XmlSerializerRetool "ID": "XmlSerializerRetool.dll", "Compatibility": { - "~": { "Status": "Obsolete", "ReasonPhrase": "it's no longer maintained or used." } + "~": { + "Status": "Obsolete", + "ReasonPhrase": "it's no longer maintained or used." + } } }, { @@ -1175,12 +1928,15 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Defaults": { "ChucklefishID": 4247 }, "Compatibility": { "~1.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 + }, + "MapRemoteVersions": { + "1.3.5": "1.3.4" // not updated in manifest } }, { // Zoom Out Extreme - "ID": "ZoomMod", - "AlternativeUrl": "http://community.playstarbound.com/threads/115028", + "ID": "ZoomMod | RockinMods.ZoomMod", // changed circa 1.2.1 + "Defaults": { "NexusID": 1326 }, "Compatibility": { "~0.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -1201,6 +1957,11 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.2 } }, + { + // Zoryn's Durable Fences + "ID": "56d3439c-7b9b-497e-9496-0c4890e8a00e | Zoryn.DurableFences", // changed in 1.6 + "Defaults": { "GitHubProject": "Zoryn4163/SMAPI-Mods" } + }, { // Zoryn's Health Bars "ID": "HealthBars.dll | Zoryn.HealthBars", // changed in 1.6 @@ -1209,6 +1970,11 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.2 } }, + { + // Zoryn's Fishing Mod + "ID": "fa277b1f-265e-47c3-a84f-cd320cc74949 | Zoryn.FishingMod", // changed in 1.6 + "Defaults": { "GitHubProject": "Zoryn4163/SMAPI-Mods" } + }, { // Zoryn's Junimo Deposit Anywhere "ID": "f93a4fe8-cade-4146-9335-b5f82fbbf7bc | Zoryn.JunimoDepositAnywhere", // changed in 1.6 From da12f9137963e273a16da7e3ad7cb839216b96d2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 25 Sep 2017 17:43:52 -0400 Subject: [PATCH 116/186] don't check for updates to Entoarox mods per request (#361) --- .../StardewModdingAPI.config.json | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/StardewModdingAPI/StardewModdingAPI.config.json b/src/StardewModdingAPI/StardewModdingAPI.config.json index 2e1c3489..9003095c 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.config.json +++ b/src/StardewModdingAPI/StardewModdingAPI.config.json @@ -99,7 +99,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Advanced Location Loader "ID": "Entoarox.AdvancedLocationLoader", - "Defaults": { "ChucklefishID": 3619 }, + //"Defaults": { "ChucklefishID": 3619 }, // Entoarox opted out of mod update checks "Compatibility": { "~1.2.10": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -621,7 +621,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Entoarox Framework "ID": "eacdb74b-4080-4452-b16b-93773cda5cf9 | Entoarox.EntoaroxFramework", // changed in ??? - "Defaults": { "ChucklefishID": 4228 }, + //"Defaults": { "ChucklefishID": 4228 }, // Entoarox opted out of mod update checks "Compatibility": { "~1.7.9": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -661,8 +661,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, { // Extended Minecart - "ID": "{ID:'EntoaroxFurnitureAnywhere', Name:'Extended Minecart'} | Entoarox.ExtendedMinecart", // changed in 1.6.1 - "Defaults": { "ChucklefishID": 4359 } + "ID": "{ID:'EntoaroxFurnitureAnywhere', Name:'Extended Minecart'} | Entoarox.ExtendedMinecart" // changed in 1.6.1 + //"Defaults": { "ChucklefishID": 4359 } // Entoarox opted out of mod update checks }, { // Fall 28 Snow Day @@ -721,8 +721,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, { // Faster Paths - "ID": "{ID:'821ce8f6-e629-41ad-9fde-03b54f68b0b6', Name:'Faster Paths'} | 615f85f8-5c89-44ee-aecc-c328f172e413 | Entoarox.FasterPaths", // changed in 1.2 and 1.3; disambiguate from Shop Expander - "Defaults": { "ChucklefishID": 3641 } + "ID": "{ID:'821ce8f6-e629-41ad-9fde-03b54f68b0b6', Name:'Faster Paths'} | 615f85f8-5c89-44ee-aecc-c328f172e413 | Entoarox.FasterPaths" // changed in 1.2 and 1.3; disambiguate from Shop Expander + // "Defaults": { "ChucklefishID": 3641 } // Entoarox opted out of mod update checks }, { // Faster Run @@ -771,8 +771,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, { // Furniture Anywhere - "ID": "{ID:'EntoaroxFurnitureAnywhere', Name:'Furniture Anywhere'} | Entoarox.FurnitureAnywhere", // changed in 1.1; disambiguate from Extended Minecart - "Defaults": { "ChucklefishID": 4324 } + "ID": "{ID:'EntoaroxFurnitureAnywhere', Name:'Furniture Anywhere'} | Entoarox.FurnitureAnywhere" // changed in 1.1; disambiguate from Extended Minecart + // "Defaults": { "ChucklefishID": 4324 } // Entoarox opted out of mod update checks }, { // Game Reminder @@ -1067,7 +1067,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // More Pets "ID": "821ce8f6-e629-41ad-9fde-03b54f68b0b6MOREPETS | Entoarox.MorePets", // changed in 1.3 - "Defaults": { "ChucklefishID": 4288 }, + // "Defaults": { "ChucklefishID": 4288 }, // Entoarox opted out of mod update checks "Compatibility": { "~1.3.2": { "Status": "AssumeBroken" } // overhauled for SMAPI 1.11+ compatibility } @@ -1409,7 +1409,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Seasonal Immersion "ID": "EntoaroxSeasonalHouse | EntoaroxSeasonalBuildings | EntoaroxSeasonalImmersion | Entoarox.SeasonalImmersion", // changed in 1.1, 1.6 or earlier, and 1.7 - "Defaults": { "ChucklefishID": 4262 }, + // "Defaults": { "ChucklefishID": 4262 }, // Entoarox opted out of mod update checks "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.8.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -1475,7 +1475,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Shop Expander "ID": "{ID:'821ce8f6-e629-41ad-9fde-03b54f68b0b6', Name:'Shop Expander'} | EntoaroxShopExpander | Entoarox.ShopExpander", // changed in 1.5 and 1.5.2; disambiguate from Faster Paths - "Defaults": { "ChucklefishID": 4381 }, + // "Defaults": { "ChucklefishID": 4381 }, // Entoarox opted out of mod update checks "Compatibility": { "~1.5.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -1916,7 +1916,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Xnb Loader "ID": "Entoarox.XnbLoader", - "Defaults": { "ChucklefishID": 4506 }, + // "Defaults": { "ChucklefishID": 4506 }, // Entoarox opted out of mod update checks "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0.6": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 From 4fd3fdc0d8367fdf0c454a1c728113c282df76b7 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 25 Sep 2017 18:18:43 -0400 Subject: [PATCH 117/186] use SMAPI version in web API (#336) --- src/StardewModdingAPI.Web/Properties/AssemblyInfo.cs | 4 ++++ src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj | 5 +++++ 2 files changed, 9 insertions(+) create mode 100644 src/StardewModdingAPI.Web/Properties/AssemblyInfo.cs diff --git a/src/StardewModdingAPI.Web/Properties/AssemblyInfo.cs b/src/StardewModdingAPI.Web/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..63f787a4 --- /dev/null +++ b/src/StardewModdingAPI.Web/Properties/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Reflection; + +[assembly: AssemblyTitle("StardewModdingAPI.Web")] +[assembly: AssemblyProduct("StardewModdingAPI.Web")] diff --git a/src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj b/src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj index bf67449b..746b1a69 100644 --- a/src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj +++ b/src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj @@ -2,8 +2,13 @@ netcoreapp2.0 + false + + + + From 5cb183e16ddc661c38f4bd9e6e740b9457b8c437 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 25 Sep 2017 21:11:48 -0400 Subject: [PATCH 118/186] consolidate update fields in manifest & SMAPI config (#336, #361) --- .../Core/ModResolverTests.cs | 14 +- .../Controllers/ModsController.cs | 6 +- src/StardewModdingAPI/Constants.cs | 9 + .../Framework/ModLoading/ModResolver.cs | 31 +- .../Framework/Models/Manifest.cs | 10 +- .../Framework/Models/ModDataDefaults.cs | 18 - .../Framework/Models/ModDataRecord.cs | 4 +- src/StardewModdingAPI/IManifest.cs | 10 +- src/StardewModdingAPI/Program.cs | 23 +- .../StardewModdingAPI.config.json | 494 +++++++++--------- .../StardewModdingAPI.csproj | 1 - 11 files changed, 294 insertions(+), 326 deletions(-) delete mode 100644 src/StardewModdingAPI/Framework/Models/ModDataDefaults.cs diff --git a/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs b/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs index 6a3fded6..051ffe99 100644 --- a/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs +++ b/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs @@ -119,7 +119,7 @@ namespace StardewModdingAPI.Tests.Core [Test(Description = "Assert that validation doesn't fail if there are no mods installed.")] public void ValidateManifests_NoMods_DoesNothing() { - new ModResolver().ValidateManifests(new ModMetadata[0], apiVersion: new SemanticVersion("1.0")); + new ModResolver().ValidateManifests(new ModMetadata[0], apiVersion: new SemanticVersion("1.0"), vendorModUrls: new Dictionary()); } [Test(Description = "Assert that validation skips manifests that have already failed without calling any other properties.")] @@ -130,7 +130,7 @@ namespace StardewModdingAPI.Tests.Core mock.Setup(p => p.Status).Returns(ModMetadataStatus.Failed); // act - new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0")); + new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), vendorModUrls: new Dictionary()); // assert mock.VerifyGet(p => p.Status, Times.Once, "The validation did not check the manifest status."); @@ -148,7 +148,7 @@ namespace StardewModdingAPI.Tests.Core }); // act - new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0")); + new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), vendorModUrls: new Dictionary()); // assert mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "The validation did not fail the metadata."); @@ -163,7 +163,7 @@ namespace StardewModdingAPI.Tests.Core this.SetupMetadataForValidation(mock); // act - new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0")); + new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), vendorModUrls: new Dictionary()); // assert mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "The validation did not fail the metadata."); @@ -177,7 +177,7 @@ namespace StardewModdingAPI.Tests.Core this.SetupMetadataForValidation(mock); // act - new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0")); + new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), vendorModUrls: new Dictionary()); // assert mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "The validation did not fail the metadata."); @@ -194,7 +194,7 @@ namespace StardewModdingAPI.Tests.Core this.SetupMetadataForValidation(mod); // act - new ModResolver().ValidateManifests(new[] { modA.Object, modB.Object }, apiVersion: new SemanticVersion("1.0")); + new ModResolver().ValidateManifests(new[] { modA.Object, modB.Object }, apiVersion: new SemanticVersion("1.0"), vendorModUrls: new Dictionary()); // assert modA.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "The validation did not fail the first mod with a unique ID."); @@ -220,7 +220,7 @@ namespace StardewModdingAPI.Tests.Core mock.Setup(p => p.DirectoryPath).Returns(modFolder); // act - new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0")); + new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), vendorModUrls: new Dictionary()); // assert // if Moq doesn't throw a method-not-setup exception, the validation didn't override the status. diff --git a/src/StardewModdingAPI.Web/Controllers/ModsController.cs b/src/StardewModdingAPI.Web/Controllers/ModsController.cs index f29de45a..26cdfa31 100644 --- a/src/StardewModdingAPI.Web/Controllers/ModsController.cs +++ b/src/StardewModdingAPI.Web/Controllers/ModsController.cs @@ -81,7 +81,7 @@ namespace StardewModdingAPI.Web.Controllers [HttpGet] public async Task> GetAsync(string modKeys) { - string[] modKeysArray = modKeys?.Split(',').Select(p => p.Trim()).ToArray(); + string[] modKeysArray = modKeys?.Split(',').ToArray(); if (modKeysArray == null || !modKeysArray.Any()) return new Dictionary(); @@ -154,8 +154,8 @@ namespace StardewModdingAPI.Web.Controllers } // parse - vendorKey = parts[0]; - modID = parts[1]; + vendorKey = parts[0].Trim(); + modID = parts[1].Trim(); return true; } } diff --git a/src/StardewModdingAPI/Constants.cs b/src/StardewModdingAPI/Constants.cs index fc780f40..5b1d7737 100644 --- a/src/StardewModdingAPI/Constants.cs +++ b/src/StardewModdingAPI/Constants.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; @@ -86,6 +87,14 @@ namespace StardewModdingAPI Platform.Mono; #endif + /// Maps vendor keys (like Nexus) to their mod URL template (where {0} is the mod ID) during mod compatibility checks. This doesn't affect update checks, which defer to the remote web API. + internal static readonly IDictionary VendorModUrls = new Dictionary(StringComparer.InvariantCultureIgnoreCase) + { + ["Chucklefish"] = "https://community.playstarbound.com/resources/{0}", + ["Nexus"] = "http://nexusmods.com/stardewvalley/mods/{0}", + ["GitHub"] = "https://github.com/{0}/releases" + }; + /********* ** Internal methods diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs index 4d20e7c8..d0ef1b08 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs @@ -61,13 +61,9 @@ namespace StardewModdingAPI.Framework.ModLoading dataRecord = dataRecords.FirstOrDefault(record => record.ID.Matches(key, manifest)); } - // apply defaults - if (dataRecord?.Defaults != null) - { - manifest.ChucklefishID = manifest.ChucklefishID ?? dataRecord.Defaults.ChucklefishID; - manifest.GitHubProject = manifest.GitHubProject ?? dataRecord.Defaults.GitHubProject; - manifest.NexusID = manifest.NexusID ?? dataRecord.Defaults.NexusID; - } + // add default update keys + if (manifest != null && manifest.UpdateKeys == null && dataRecord?.UpdateKeys != null) + manifest.UpdateKeys = dataRecord.UpdateKeys; // build metadata string displayName = !string.IsNullOrWhiteSpace(manifest?.Name) @@ -84,7 +80,8 @@ namespace StardewModdingAPI.Framework.ModLoading /// Validate manifest metadata. /// The mod manifests to validate. /// The current SMAPI version. - public void ValidateManifests(IEnumerable mods, ISemanticVersion apiVersion) + /// Maps vendor keys (like Nexus) to their mod URL template (where {0} is the mod ID). + public void ValidateManifests(IEnumerable mods, ISemanticVersion apiVersion, IDictionary vendorModUrls) { mods = mods.ToArray(); @@ -110,12 +107,18 @@ namespace StardewModdingAPI.Framework.ModLoading // get update URLs List updateUrls = new List(); - if (!string.IsNullOrWhiteSpace(mod.Manifest.ChucklefishID)) - updateUrls.Add($"https://community.playstarbound.com/resources/{mod.Manifest.ChucklefishID}"); - if (!string.IsNullOrWhiteSpace(mod.Manifest.NexusID)) - updateUrls.Add($"http://nexusmods.com/stardewvalley/mods/{mod.Manifest.NexusID}"); - if (!string.IsNullOrWhiteSpace(mod.Manifest.GitHubProject)) - updateUrls.Add($"https://github.com/{mod.Manifest.GitHubProject}/releases"); + foreach (string key in mod.Manifest.UpdateKeys ?? new string[0]) + { + string[] parts = key.Split(new[] { ':' }, 2); + if (parts.Length != 2) + continue; + + string vendorKey = parts[0].Trim(); + string modID = parts[1].Trim(); + + if (vendorModUrls.TryGetValue(vendorKey, out string urlTemplate)) + updateUrls.Add(string.Format(urlTemplate, modID)); + } if (mod.DataRecord.AlternativeUrl != null) updateUrls.Add(mod.DataRecord.AlternativeUrl); diff --git a/src/StardewModdingAPI/Framework/Models/Manifest.cs b/src/StardewModdingAPI/Framework/Models/Manifest.cs index c891644f..a051354c 100644 --- a/src/StardewModdingAPI/Framework/Models/Manifest.cs +++ b/src/StardewModdingAPI/Framework/Models/Manifest.cs @@ -34,14 +34,8 @@ namespace StardewModdingAPI.Framework.Models [JsonConverter(typeof(SFieldConverter))] public IManifestDependency[] Dependencies { get; set; } - /// The mod's unique ID in the Chucklefish mod site (if any), used for update checks. - public string ChucklefishID { get; set; } - - /// The mod's unique ID in Nexus Mods (if any), used for update checks. - public string NexusID { get; set; } - - /// The mod's organisation and project name on GitHub (if any), used for update checks. - public string GitHubProject { get; set; } + /// The namespaced mod IDs to query for updates (like Nexus:541). + public string[] UpdateKeys { get; set; } /// The unique mod ID. public string UniqueID { get; set; } diff --git a/src/StardewModdingAPI/Framework/Models/ModDataDefaults.cs b/src/StardewModdingAPI/Framework/Models/ModDataDefaults.cs deleted file mode 100644 index e0ab94b8..00000000 --- a/src/StardewModdingAPI/Framework/Models/ModDataDefaults.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace StardewModdingAPI.Framework.Models -{ - /// Default values for support fields to inject into the manifest. - internal class ModDataDefaults - { - /********* - ** Accessors - *********/ - /// The mod's unique ID in the Chucklefish mod site (if any), used for update checks. - public string ChucklefishID { get; set; } - - /// The mod's unique ID in Nexus Mods (if any), used for update checks. - public string NexusID { get; set; } - - /// The mod's organisation and project name on GitHub (if any), used for update checks. - public string GitHubProject { get; set; } - } -} diff --git a/src/StardewModdingAPI/Framework/Models/ModDataRecord.cs b/src/StardewModdingAPI/Framework/Models/ModDataRecord.cs index 0d033e82..c6a12188 100644 --- a/src/StardewModdingAPI/Framework/Models/ModDataRecord.cs +++ b/src/StardewModdingAPI/Framework/Models/ModDataRecord.cs @@ -15,8 +15,8 @@ namespace StardewModdingAPI.Framework.Models [JsonConverter(typeof(SFieldConverter))] public ModDataID ID { get; set; } - /// Default values for support fields to inject into the manifest. - public ModDataDefaults Defaults { get; set; } + /// A value to inject into field if it's not already set. + public string[] UpdateKeys { get; set; } /// The URL where the player can get an unofficial or alternative version of the mod if the official version isn't compatible. public string AlternativeUrl { get; set; } diff --git a/src/StardewModdingAPI/IManifest.cs b/src/StardewModdingAPI/IManifest.cs index 9513834a..befef901 100644 --- a/src/StardewModdingAPI/IManifest.cs +++ b/src/StardewModdingAPI/IManifest.cs @@ -32,14 +32,8 @@ namespace StardewModdingAPI /// The other mods that must be loaded before this mod. IManifestDependency[] Dependencies { get; } - /// The mod's unique ID in the Chucklefish mod site (if any), used for update checks. - string ChucklefishID { get; } - - /// The mod's unique ID in Nexus Mods (if any), used for update checks. - string NexusID { get; } - - /// The mod's organisation and project name on GitHub (if any), used for update checks. - string GitHubProject { get; } + /// The namespaced mod IDs to query for updates (like Nexus:541). + string[] UpdateKeys { get; set; } /// Any manifest fields which didn't match a valid field. IDictionary ExtraFields { get; } diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 0c4648ab..3575add6 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -347,7 +347,7 @@ namespace StardewModdingAPI // load manifests IModMetadata[] mods = resolver.ReadManifests(Constants.ModPath, new JsonHelper(), this.Settings.ModData).ToArray(); - resolver.ValidateManifests(mods, Constants.ApiVersion); + resolver.ValidateManifests(mods, Constants.ApiVersion, Constants.VendorModUrls); // process dependencies mods = resolver.ProcessDependencies(mods).ToArray(); @@ -530,25 +530,12 @@ namespace StardewModdingAPI } // add update keys - bool hasUpdateKeys = false; - if (!string.IsNullOrWhiteSpace(mod.Manifest.ChucklefishID)) + if (mod.Manifest.UpdateKeys?.Any() == true) { - hasUpdateKeys = true; - modsByKey[$"Chucklefish:{mod.Manifest.ChucklefishID}"] = mod; + foreach (string updateKey in mod.Manifest.UpdateKeys) + modsByKey[updateKey] = mod; } - if (!string.IsNullOrWhiteSpace(mod.Manifest.NexusID)) - { - hasUpdateKeys = true; - modsByKey[$"Nexus:{mod.Manifest.NexusID}"] = mod; - } - if (!string.IsNullOrWhiteSpace(mod.Manifest.GitHubProject)) - { - hasUpdateKeys = true; - modsByKey[$"GitHub:{mod.Manifest.GitHubProject}"] = mod; - } - - // log - if (!hasUpdateKeys) + else this.VerboseLog($" {mod.DisplayName}: no update keys."); } diff --git a/src/StardewModdingAPI/StardewModdingAPI.config.json b/src/StardewModdingAPI/StardewModdingAPI.config.json index 9003095c..54fe52c5 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.config.json +++ b/src/StardewModdingAPI/StardewModdingAPI.config.json @@ -48,8 +48,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha * or a JSON structure containing any of three manifest fields (ID, Name, and Author) to * match. * - * - 'Defaults' specifies fields to inject into the mod's manifest if they're not already set. - * Supported fields: ChucklefishID, GitHubProject, and NexusID. + * - 'UpdateKeys' specifies the value of the equivalent manifest field if it's not already set. + * This is used to enable update checks for older mods that haven't been updated to use it yet. * * - 'AlternativeUrl' specifies a URL where the player can find an unofficial update or * alternative if the mod is no longer compatible. @@ -73,7 +73,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // AccessChestAnywhere "ID": "AccessChestAnywhere", - "Defaults": { "NexusID": 257 }, + "UpdateKeys": [ "Nexus:257" ], "AlternativeUrl": "https://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.1": { "Status": "AssumeBroken" } // broke in SDV 1.1 @@ -85,7 +85,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // AdjustArtisanPrices "ID": "1e36d4ca-c7ef-4dfb-9927-d27a6c3c8bdc", - "Defaults": { "ChucklefishID": 3532 }, + "UpdateKeys": [ "Chucklefish:3532" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~0.1": { "Status": "AssumeBroken" } // broke in SMAPI 1.9 @@ -94,12 +94,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Adjust Monster "ID": "mmanlapat.AdjustMonster", - "Defaults": { "NexusID": 1161 } + "UpdateKeys": [ "Nexus:1161" ] }, { // Advanced Location Loader "ID": "Entoarox.AdvancedLocationLoader", - //"Defaults": { "ChucklefishID": 3619 }, // Entoarox opted out of mod update checks + //"UpdateKeys": [ "Chucklefish:3619" ], // Entoarox opted out of mod update checks "Compatibility": { "~1.2.10": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -107,12 +107,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Adventure Shop Inventory "ID": "HammurabiAdventureShopInventory", - "Defaults": { "ChucklefishID": 4608 } + "UpdateKeys": [ "Chucklefish:4608" ] }, { // AgingMod "ID": "skn.AgingMod", - "Defaults": { "NexusID": 1129 }, + "UpdateKeys": [ "Nexus:1129" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -121,17 +121,17 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // All Crops All Seasons "ID": "29ee8246-d67b-4242-a340-35a9ae0d5dd7 | community.AllCropsAllSeasons", // changed in 1.3 - "Defaults": { "NexusID": 170 } + "UpdateKeys": [ "Nexus:170" ] }, { // All Professions "ID": "8c37b1a7-4bfb-4916-9d8a-9533e6363ea3 | community.AllProfessions", // changed in 1.2 - "Defaults": { "NexusID": 174 } + "UpdateKeys": [ "Nexus:174" ] }, { // Almighty Tool "ID": "AlmightyTool.dll | 439", // changed in 1.2.1 - "Defaults": { "NexusID": 439 }, + "UpdateKeys": [ "Nexus:439" ], "Compatibility": { "~1.1.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 }, @@ -152,7 +152,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Animal Sitter "ID": "AnimalSitter.dll | jwdred.AnimalSitter", // changed in 1.0.9 - "Defaults": { "NexusID": 581 }, + "UpdateKeys": [ "Nexus:581" ], "Compatibility": { "~1.0.8": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -160,7 +160,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // A Tapper's Dream "ID": "ddde5195-8f85-4061-90cc-0d4fd5459358", - "Defaults": { "NexusID": 260 }, + "UpdateKeys": [ "Nexus:260" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -169,7 +169,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Auto Animal Doors "ID": "AaronTaggart.AutoAnimalDoors", - "Defaults": { "NexusID": 1019 }, + "UpdateKeys": [ "Nexus:1019" ], "MapRemoteVersions": { "1.1.1": "1.1" // manifest not updated } @@ -177,22 +177,22 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Auto-Eat "ID": "BALANCEMOD_AutoEat | Permamiss.AutoEat", // changed in 1.1.1 - "Defaults": { "NexusID": 643 } + "UpdateKeys": [ "Nexus:643" ] }, { // AutoGate "ID": "AutoGate", - "Defaults": { "NexusID": 820 } + "UpdateKeys": [ "Nexus:820" ] }, { // Automate "ID": "Pathoschild.Automate", - "Defaults": { "NexusID": 1063 } + "UpdateKeys": [ "Nexus:1063" ] }, { // Automated Doors "ID": "1abcfa07-2cf4-4dc3-a6e9-6068b642112b | azah.automated-doors", // changed in 1.4.1 - "Defaults": { "GitHubProject": "azah/AutomatedDoors" }, + "UpdateKeys": [ "GitHub:azah/AutomatedDoors" ], "MapLocalVersions": { "1.4.1-1": "1.4.1" } @@ -200,12 +200,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // AutoSpeed "ID": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'AutoSpeed'} | Omegasis.AutoSpeed", // changed in 1.4; disambiguate from other Alpha_Omegasis mods - "Defaults": { "NexusID": 443 } + "UpdateKeys": [ "Nexus:443" ] }, { // Basic Sprinkler Improved "ID": "lrsk_sdvm_bsi.0117171308", - "Defaults": { "NexusID": 833 }, + "UpdateKeys": [ "Nexus:833" ], "MapRemoteVersions": { "1.0.2": "1.0.1-release" // manifest not updated } @@ -213,22 +213,22 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Better Quality More Seasons "ID": "SB_BQMS", - "Defaults": { "NexusID": 935 } + "UpdateKeys": [ "Nexus:935" ] }, { // Better Quarry "ID": "BetterQuarry", - "Defaults": { "NexusID": 771 } + "UpdateKeys": [ "Nexus:771" ] }, { // Better Ranching "ID": "BetterRanching", - "Defaults": { "NexusID": 859 } + "UpdateKeys": [ "Nexus:859" ] }, { // Better Shipping Box "ID": "Kithio:BetterShippingBox", - "Defaults": { "ChucklefishID": 4302 }, + "UpdateKeys": [ "Chucklefish:4302" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -240,7 +240,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Better Sprinklers "ID": "SPDSprinklersMod | Speeder.BetterSprinklers", // changed in 2.3 - "Defaults": { "NexusID": 41 }, + "UpdateKeys": [ "Nexus:41" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~2.3.1-pathoschild-update": { "Status": "AssumeBroken" } // broke in SDV 1.2 @@ -249,12 +249,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Billboard Anywhere "ID": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Billboard Anywhere'} | Omegasis.BillboardAnywhere", // changed in 1.4; disambiguate from other mods by Alpha_Omegasis - "Defaults": { "NexusID": 492 } + "UpdateKeys": [ "Nexus:492" ] }, { // Birthday Mail "ID": "005e02dc-d900-425c-9c68-1ff55c5a295d | KathrynHazuka.BirthdayMail", // changed in 1.2.3-pathoschild-update - "Defaults": { "NexusID": 276 }, + "UpdateKeys": [ "Nexus:276" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.2.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 @@ -263,12 +263,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Breed Like Rabbits "ID": "dycedarger.breedlikerabbits", - "Defaults": { "NexusID": 948 } + "UpdateKeys": [ "Nexus:948" ] }, { // Build Endurance "ID": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'BuildEndurance'} | Omegasis.BuildEndurance", // changed in 1.4; disambiguate from other Alpha_Omegasis mods - "Defaults": { "NexusID": 445 }, + "UpdateKeys": [ "Nexus:445" ], "Compatibility": { "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -276,7 +276,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Build Health "ID": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'BuildHealth'} | Omegasis.BuildHealth", // changed in 1.4; disambiguate from other Alpha_Omegasis mods - "Defaults": { "NexusID": 446 }, + "UpdateKeys": [ "Nexus:446" ], "Compatibility": { "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -284,7 +284,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Buy Cooking Recipes "ID": "Denifia.BuyRecipes", - "Defaults": { "NexusID": 1126 }, + "UpdateKeys": [ "Nexus:1126" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -293,7 +293,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Buy Back Collectables "ID": "BuyBackCollectables | Omegasis.BuyBackCollectables", // changed in 1.4 - "Defaults": { "NexusID": 507 }, + "UpdateKeys": [ "Nexus:507" ], "Compatibility": { "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -301,12 +301,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Carry Chest "ID": "spacechase0.CarryChest", - "Defaults": { "NexusID": 1333 } + "UpdateKeys": [ "Nexus:1333" ] }, { // Casks Anywhere "ID": "CasksAnywhere", - "Defaults": { "NexusID": 878 }, + "UpdateKeys": [ "Nexus:878" ], "MapLocalVersions": { "1.1-alpha": "1.1" } @@ -314,12 +314,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Categorize Chests "ID": "CategorizeChests", - "Defaults": { "NexusID": 1300 } + "UpdateKeys": [ "Nexus:1300" ] }, { // ChefsCloset "ID": "Duder.ChefsCloset", - "Defaults": { "NexusID": 1030 }, + "UpdateKeys": [ "Nexus:1030" ], "MapLocalVersions": { "1.3-1": "1.3" } @@ -327,7 +327,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Chest Label System "ID": "SPDChestLabel | Speeder.ChestLabel", // changed in 1.5.1-pathoschild-update - "Defaults": { "NexusID": 242 }, + "UpdateKeys": [ "Nexus:242" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.1 @@ -336,7 +336,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Chest Pooling "ID": "ChestPooling.dll | mralbobo.ChestPooling", // changed in 1.3 - "Defaults": { "GitHubProject": "mralbobo/stardew-chest-pooling" }, + "UpdateKeys": [ "GitHub:mralbobo/stardew-chest-pooling" ], "Compatibility": { "~1.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -344,7 +344,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Chests Anywhere "ID": "ChestsAnywhere | Pathoschild.ChestsAnywhere", // changed in 1.9 - "Defaults": { "NexusID": 518 }, + "UpdateKeys": [ "Nexus:518" ], "Compatibility": { "~1.9-beta": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -352,7 +352,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Choose Baby Gender "ID": "ChooseBabyGender.dll", - "Defaults": { "NexusID": 590 }, + "UpdateKeys": [ "Nexus:590" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -361,7 +361,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // CJB Automation "ID": "CJBAutomation", - "Defaults": { "NexusID": 211 }, + "UpdateKeys": [ "Nexus:211" ], "AlternativeUrl": "http://www.nexusmods.com/stardewvalley/mods/1063", "Compatibility": { "~1.4": { "Status": "AssumeBroken" } // broke in SDV 1.2 @@ -370,7 +370,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // CJB Cheats Menu "ID": "CJBCheatsMenu | CJBok.CheatsMenu", // changed in 1.14 - "Defaults": { "NexusID": 4 }, + "UpdateKeys": [ "Nexus:4" ], "Compatibility": { "~1.12": { "Status": "AssumeBroken" } // broke in SDV 1.1 } @@ -378,7 +378,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // CJB Item Spawner "ID": "CJBItemSpawner | CJBok.ItemSpawner", // changed in 1.7 - "Defaults": { "NexusID": 93 }, + "UpdateKeys": [ "Nexus:93" ], "Compatibility": { "~1.5": { "Status": "AssumeBroken" } // broke in SDV 1.1 } @@ -386,7 +386,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // CJB Show Item Sell Price "ID": "CJBShowItemSellPrice | CJBok.ShowItemSellPrice", // changed in 1.7 - "Defaults": { "NexusID": 5 }, + "UpdateKeys": [ "Nexus:5" ], "Compatibility": { "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -394,12 +394,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Clean Farm "ID": "tstaples.CleanFarm", - "Defaults": { "NexusID": 794 } + "UpdateKeys": [ "Nexus:794" ] }, { // Climates of Ferngill "ID": "KoihimeNakamura.ClimatesOfFerngill", - "Defaults": { "NexusID": 604 }, + "UpdateKeys": [ "Nexus:604" ], "Compatibility": { "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -407,7 +407,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Cold Weather Haley "ID": "LordXamon.ColdWeatherHaleyPRO", - "Defaults": { "NexusID": 1169 }, + "UpdateKeys": [ "Nexus:1169" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -426,7 +426,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Combat with Farm Implements "ID": "SPDFarmingImplementsInCombat", - "Defaults": { "NexusID": 313 }, + "UpdateKeys": [ "Nexus:313" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -435,12 +435,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Community Bundle Item Tooltip "ID": "musbah.bundleTooltip", - "Defaults": { "NexusID": 1329 } + "UpdateKeys": [ "Nexus:1329" ] }, { // Configurable Machines "ID": "21da6619-dc03-4660-9794-8e5b498f5b97", - "Defaults": { "NexusID": 280 }, + "UpdateKeys": [ "Nexus:280" ], "MapLocalVersions": { "1.2-beta": "1.2" } @@ -448,7 +448,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Configurable Shipping Dates "ID": "ConfigurableShippingDates", - "Defaults": { "NexusID": 675 }, + "UpdateKeys": [ "Nexus:675" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -457,7 +457,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Cooking Skill "ID": "CookingSkill | spacechase0.CookingSkill", // changed in 1.0.4–6 - "Defaults": { "NexusID": 522 }, + "UpdateKeys": [ "Nexus:522" ], "Compatibility": { "~1.0.6": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -465,7 +465,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // CrabNet "ID": "CrabNet.dll | jwdred.CrabNet", // changed in 1.0.5 - "Defaults": { "NexusID": 584 }, + "UpdateKeys": [ "Nexus:584" ], "Compatibility": { "~1.0.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -473,37 +473,37 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Current Location "ID": "CurrentLocation102120161203", - "Defaults": { "NexusID": 638 } + "UpdateKeys": [ "Nexus:638" ] }, { // Custom Critters "ID": "spacechase0.CustomCritters", - "Defaults": { "NexusID": 1255 } + "UpdateKeys": [ "Nexus:1255" ] }, { // Custom Element Handler "ID": "Platonymous.CustomElementHandler", - "Defaults": { "NexusID": 1068 } + "UpdateKeys": [ "Nexus:1068" ] }, { // Custom Farming "ID": "Platonymous.CustomFarming", - "Defaults": { "NexusID": 991 } + "UpdateKeys": [ "Nexus:991" ] }, { // Custom Farm Types "ID": "spacechase0.CustomFarmTypes", - "Defaults": { "NexusID": 1140 } + "UpdateKeys": [ "Nexus:1140" ] }, { // Custom Furniture "ID": "Platonymous.CustomFurniture", - "Defaults": { "NexusID": 1254 } + "UpdateKeys": [ "Nexus:1254" ] }, { // Customize Exterior "ID": "CustomizeExterior | spacechase0.CustomizeExterior", // changed in 1.0.3 - "Defaults": { "NexusID": 1099 }, + "UpdateKeys": [ "Nexus:1099" ], "Compatibility": { "~1.0.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -511,7 +511,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Customizable Cart Redux "ID": "KoihimeNakamura.CCR", - "Defaults": { "NexusID": 1402 }, + "UpdateKeys": [ "Nexus:1402" ], "MapLocalVersions": { "1.1-20170917": "1.1" } @@ -519,7 +519,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Customizable Traveling Cart Days "ID": "TravelingCartYyeahdude", - "Defaults": { "NexusID": 567 }, + "UpdateKeys": [ "Nexus:567" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -528,7 +528,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Custom Linens "ID": "Mevima.CustomLinens", - "Defaults": { "NexusID": 1027 }, + "UpdateKeys": [ "Nexus:1027" ], "MapRemoteVersions": { "1.1": "1.0" // manifest not updated } @@ -536,22 +536,22 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Custom Shops Redux "ID": "Omegasis.CustomShopReduxGui", - "Defaults": { "NexusID": 1378 } + "UpdateKeys": [ "Nexus:1378" ] }, { // Custom TV "ID": "Platonymous.CustomTV", - "Defaults": { "NexusID": 1139 } + "UpdateKeys": [ "Nexus:1139" ] }, { // Daily Luck Message "ID": "Schematix.DailyLuckMessage", - "Defaults": { "NexusID": 1327 } + "UpdateKeys": [ "Nexus:1327" ] }, { // Daily News "ID": "bashNinja.DailyNews", - "Defaults": { "NexusID": 1141 }, + "UpdateKeys": [ "Nexus:1141" ], "Compatibility": { "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -559,17 +559,17 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Daily Quest Anywhere "ID": "DailyQuest | Omegasis.DailyQuestAnywhere", // changed in 1.4 - "Defaults": { "NexusID": 513 } + "UpdateKeys": [ "Nexus:513" ] }, { // Debug Mode "ID": "Pathoschild.Stardew.DebugMode | Pathoschild.DebugMode", // changed in 1.4 - "Defaults": { "NexusID": 679 } + "UpdateKeys": [ "Nexus:679" ] }, { // Dynamic Checklist "ID": "gunnargolf.DynamicChecklist", - "Defaults": { "NexusID": 1145 }, + "UpdateKeys": [ "Nexus:1145" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -578,7 +578,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Dynamic Horses "ID": "Bpendragon-DynamicHorses", - "Defaults": { "NexusID": 874 }, + "UpdateKeys": [ "Nexus:874" ], "MapRemoteVersions": { "1.2": "1.1-release" // manifest not updated } @@ -586,7 +586,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Dynamic Machines "ID": "DynamicMachines", - "Defaults": { "NexusID": 374 }, + "UpdateKeys": [ "Nexus:374" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -598,12 +598,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Dynamic NPC Sprites "ID": "BashNinja.DynamicNPCSprites", - "Defaults": { "NexusID": 1183 } + "UpdateKeys": [ "Nexus:1183" ] }, { // Empty Hands "ID": "QuicksilverFox.EmptyHands", - "Defaults": { "NexusID": 1176 }, + "UpdateKeys": [ "Nexus:1176" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -612,7 +612,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Enemy Health Bars "ID": "SPDHealthBar | Speeder.HealthBars", // changed in 1.7.1-pathoschild-update - "Defaults": { "NexusID": 193 }, + "UpdateKeys": [ "Nexus:193" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.7": { "Status": "AssumeBroken" } // broke in SDV 1.2 @@ -621,7 +621,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Entoarox Framework "ID": "eacdb74b-4080-4452-b16b-93773cda5cf9 | Entoarox.EntoaroxFramework", // changed in ??? - //"Defaults": { "ChucklefishID": 4228 }, // Entoarox opted out of mod update checks + //"UpdateKeys": [ "Chucklefish:4228" ], // Entoarox opted out of mod update checks "Compatibility": { "~1.7.9": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -629,22 +629,22 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Expanded Fridge / Dynamic Expanded Fridge "ID": "Uwazouri.ExpandedFridge", - "Defaults": { "NexusID": 1191 } + "UpdateKeys": [ "Nexus:1191" ] }, { // Experience Bars "ID": "ExperienceBars | spacechase0.ExperienceBars", // changed in 1.0.2 - "Defaults": { "NexusID": 509 } + "UpdateKeys": [ "Nexus:509" ] }, { // Extended Bus System "ID": "ExtendedBusSystem", - "Defaults": { "ChucklefishID": 4373 } + "UpdateKeys": [ "Chucklefish:4373" ] }, { // Extended Fridge "ID": "Mystra007ExtendedFridge | Crystalmir.ExtendedFridge", // changed in 1.0.1 - "Defaults": { "NexusID": 485 }, + "UpdateKeys": [ "Nexus:485" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.2 @@ -653,7 +653,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Extended Greenhouse "ID": "ExtendedGreenhouse", - "Defaults": { "ChucklefishID": 4303 }, + "UpdateKeys": [ "Chucklefish:4303" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 @@ -662,12 +662,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Extended Minecart "ID": "{ID:'EntoaroxFurnitureAnywhere', Name:'Extended Minecart'} | Entoarox.ExtendedMinecart" // changed in 1.6.1 - //"Defaults": { "ChucklefishID": 4359 } // Entoarox opted out of mod update checks + //"UpdateKeys": [ "Chucklefish:4359" ] // Entoarox opted out of mod update checks }, { // Fall 28 Snow Day "ID": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Fall28 Snow Day'} | Omegasis.Fall28SnowDay", // changed in 1.4; disambiguate from other mods by Alpha_Omegasis - "Defaults": { "NexusID": 486 }, + "UpdateKeys": [ "Nexus:486" ], "Compatibility": { "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -699,7 +699,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Farm Expansion "ID": "3888bdfd-73f6-4776-8bb7-8ad45aea1915 | AdvizeFarmExpansionMod-2-0 | AdvizeFarmExpansionMod-2-0-5", // changed in 2.0 and 2.0.5 - "Defaults": { "NexusID": 130 }, + "UpdateKeys": [ "Nexus:130" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~2.0.5": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -708,7 +708,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Farm Resource Generator "ID": "FarmResourceGenerator.dll", - "Defaults": { "NexusID": 647 }, + "UpdateKeys": [ "Nexus:647" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -717,17 +717,17 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Fast Animations "ID": "Pathoschild.FastAnimations", - "Defaults": { "NexusID": 1089 } + "UpdateKeys": [ "Nexus:1089" ] }, { // Faster Paths "ID": "{ID:'821ce8f6-e629-41ad-9fde-03b54f68b0b6', Name:'Faster Paths'} | 615f85f8-5c89-44ee-aecc-c328f172e413 | Entoarox.FasterPaths" // changed in 1.2 and 1.3; disambiguate from Shop Expander - // "Defaults": { "ChucklefishID": 3641 } // Entoarox opted out of mod update checks + // "UpdateKeys": [ "Chucklefish:3641" ] // Entoarox opted out of mod update checks }, { // Faster Run "ID": "FasterRun.dll", - "Defaults": { "NexusID": 733 }, + "UpdateKeys": [ "Nexus:733" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -736,17 +736,17 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Fishing Adjust "ID": "shuaiz.FishingAdjustMod", - "Defaults": { "NexusID": 1350 } + "UpdateKeys": [ "Nexus:1350" ] }, { // Fishing Tuner Redux "ID": "HammurabiFishingTunerRedux", - "Defaults": { "ChucklefishID": 4578 } + "UpdateKeys": [ "Chucklefish:4578" ] }, { // FlorenceMod "ID": "FlorenceMod.dll", - "Defaults": { "NexusID": 591 }, + "UpdateKeys": [ "Nexus:591" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -758,12 +758,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Flower Color Picker "ID": "spacechase0.FlowerColorPicker", - "Defaults": { "NexusID": 1229 } + "UpdateKeys": [ "Nexus:1229" ] }, { // Forage at the Farm "ID": "ForageAtTheFarm", - "Defaults": { "NexusID": 673 }, + "UpdateKeys": [ "Nexus:673" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.5.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -772,17 +772,17 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Furniture Anywhere "ID": "{ID:'EntoaroxFurnitureAnywhere', Name:'Furniture Anywhere'} | Entoarox.FurnitureAnywhere" // changed in 1.1; disambiguate from Extended Minecart - // "Defaults": { "ChucklefishID": 4324 } // Entoarox opted out of mod update checks + // "UpdateKeys": [ "Chucklefish:4324" ] // Entoarox opted out of mod update checks }, { // Game Reminder "ID": "mmanlapat.GameReminder", - "Defaults": { "NexusID": 1153 } + "UpdateKeys": [ "Nexus:1153" ] }, { // Gate Opener "ID": "GateOpener.dll | mralbobo.GateOpener", // changed in 1.1 - "Defaults": { "GitHubProject": "mralbobo/stardew-gate-opener" }, + "UpdateKeys": [ "GitHub:mralbobo/stardew-gate-opener" ], "Compatibility": { "~1.0.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -790,7 +790,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // GenericShopExtender "ID": "GenericShopExtender", - "Defaults": { "NexusID": 814 }, + "UpdateKeys": [ "Nexus:814" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~0.1.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -799,7 +799,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Get Dressed "ID": "GetDressed.dll | Advize.GetDressed", // changed in 3.3 - "Defaults": { "NexusID": 331 }, + "UpdateKeys": [ "Nexus:331" ], "Compatibility": { "~3.3": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -807,12 +807,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Giant Crop Ring "ID": "cat.giantcropring", - "Defaults": { "NexusID": 1182 } + "UpdateKeys": [ "Nexus:1182" ] }, { // Gift Taste Helper "ID": "8008db57-fa67-4730-978e-34b37ef191d6 | tstaples.GiftTasteHelper", // changed in 2.5 - "Defaults": { "NexusID": 229 }, + "UpdateKeys": [ "Nexus:229" ], "Compatibility": { "~2.3.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -820,7 +820,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Grandfather's Gift "ID": "ShadowDragon.GrandfathersGift", - "Defaults": { "NexusID": 985 } + "UpdateKeys": [ "Nexus:985" ] }, { // Happy Animals @@ -833,7 +833,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Happy Birthday (Omegasis) "ID": "{ID:'HappyBirthday', Author:'Alpha_Omegasis'} | Omegasis.HappyBirthday", // changed in 1.4; disambiguate from Oxyligen's fork - "Defaults": { "NexusID": 520 }, + "UpdateKeys": [ "Nexus:520" ], "Compatibility": { "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -841,22 +841,22 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Happy Birthday (Oxyligen fork) "ID": "{ID:'HappyBirthday', Author:'Alpha_Omegasis/Oxyligen'}", // disambiguate from Oxyligen's fork - "Defaults": { "NexusID": 1064 } + "UpdateKeys": [ "Nexus:1064" ] }, { // Harp of Yoba Redux "ID": "Platonymous.HarpOfYobaRedux", - "Defaults": { "NexusID": 914 } + "UpdateKeys": [ "Nexus:914" ] }, { // Harvest Moon Witch Princess "ID": "Sasara.WitchPrincess", - "Defaults": { "NexusID": 1157 } + "UpdateKeys": [ "Nexus:1157" ] }, { // Harvest With Scythe "ID": "965169fd-e1ed-47d0-9f12-b104535fb4bc", - "Defaults": { "NexusID": 236 }, + "UpdateKeys": [ "Nexus:236" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0.6": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -865,17 +865,17 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Horse Whistle (icepuente) "ID": "icepuente.HorseWhistle", - "Defaults": { "NexusID": 1131 } + "UpdateKeys": [ "Nexus:1131" ] }, { // Hunger (Yyeadude) "ID": "HungerYyeadude", - "Defaults": { "NexusID": 613 } + "UpdateKeys": [ "Nexus:613" ] }, { // Hunger for Food (Tigerle) "ID": "HungerForFoodByTigerle", - "Defaults": { "NexusID": 810 }, + "UpdateKeys": [ "Nexus:810" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~0.1.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -884,7 +884,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Hunger Mod (skn) "ID": "skn.HungerMod", - "Defaults": { "NexusID": 1127 }, + "UpdateKeys": [ "Nexus:1127" ], "MapRemoteVersions": { "1.2.1": "1.0" // manifest not updated } @@ -892,7 +892,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Idle Pause "ID": "Veleek.IdlePause", - "Defaults": { "NexusID": 1092 }, + "UpdateKeys": [ "Nexus:1092" ], "MapRemoteVersions": { "1.2": "1.1" // manifest not updated } @@ -900,7 +900,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Improved Quality of Life "ID": "Demiacle.ImprovedQualityOfLife", - "Defaults": { "NexusID": 1025 }, + "UpdateKeys": [ "Nexus:1025" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -917,12 +917,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Instant Grow Trees "ID": "dc50c58b-c7d8-4e60-86cc-e27b5d95ee59 | community.InstantGrowTrees", // changed in 1.2 - "Defaults": { "NexusID": 173 } + "UpdateKeys": [ "Nexus:173" ] }, { // Interaction Helper "ID": "HammurabiInteractionHelper", - "Defaults": { "ChucklefishID": 4640 }, + "UpdateKeys": [ "Chucklefish:4640" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -931,7 +931,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Item Auto Stacker "ID": "cat.autostacker", - "Defaults": { "NexusID": 1184 }, + "UpdateKeys": [ "Nexus:1184" ], "MapRemoteVersions": { "1.0.1": "1.0" // manifest not updated } @@ -939,7 +939,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Junimo Farm "ID": "Platonymous.JunimoFarm", - "Defaults": { "NexusID": 984 }, + "UpdateKeys": [ "Nexus:984" ], "MapRemoteVersions": { "1.1.2": "1.1.1" // manifest not updated } @@ -947,7 +947,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Less Strict Over-Exertion (AntiExhaustion) "ID": "BALANCEMOD_AntiExhaustion", - "Defaults": { "NexusID": 637 }, + "UpdateKeys": [ "Nexus:637" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -959,22 +959,22 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Level Up Notifications "ID": "Level Up Notifications", - "Defaults": { "NexusID": 855 } + "UpdateKeys": [ "Nexus:855" ] }, { // Location and Music Logging "ID": "Brandy Lover.LMlog", - "Defaults": { "NexusID": 1366 } + "UpdateKeys": [ "Nexus:1366" ] }, { // Longevity "ID": "RTGOAT.Longevity", - "Defaults": { "NexusID": 649 } + "UpdateKeys": [ "Nexus:649" ] }, { // Lookup Anything "ID": "LookupAnything | Pathoschild.LookupAnything", // changed in 1.10.1 - "Defaults": { "NexusID": 541 }, + "UpdateKeys": [ "Nexus:541" ], "Compatibility": { "~1.10.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -982,12 +982,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Love Bubbles "ID": "LoveBubbles", - "Defaults": { "NexusID": 1318 } + "UpdateKeys": [ "Nexus:1318" ] }, { // Loved Labels "ID": "LovedLabels.dll", - "Defaults": { "NexusID": 279 }, + "UpdateKeys": [ "Nexus:279" ], "Compatibility": { "~2.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -995,7 +995,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Luck Skill "ID": "LuckSkill | spacechase0.LuckSkill", // changed in 0.1.4 - "Defaults": { "NexusID": 521 }, + "UpdateKeys": [ "Nexus:521" ], "Compatibility": { "~0.1.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -1003,7 +1003,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // MailOrderPigs "ID": "MailOrderPigs.dll | jwdred.MailOrderPigs", // changed in 1.0.2 - "Defaults": { "NexusID": 632 }, + "UpdateKeys": [ "Nexus:632" ], "Compatibility": { "~1.0.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -1011,7 +1011,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Makeshift Multiplayer "ID": "StardewValleyMP | spacechase0.StardewValleyMP", // changed in 0.3 - "Defaults": { "NexusID": 501 }, + "UpdateKeys": [ "Nexus:501" ], "Compatibility": { "~0.3.6": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -1019,12 +1019,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Map Image Exporter "ID": "MapImageExporter | spacechase0.MapImageExporter", // changed in 1.0.2 - "Defaults": { "NexusID": 1073 } + "UpdateKeys": [ "Nexus:1073" ] }, { // Message Box [API]? (ChatMod) "ID": "Kithio:ChatMod", - "Defaults": { "ChucklefishID": 4296 }, + "UpdateKeys": [ "Chucklefish:4296" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -1033,12 +1033,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Mining at the Farm "ID": "MiningAtTheFarm", - "Defaults": { "NexusID": 674 } + "UpdateKeys": [ "Nexus:674" ] }, { // Mining With Explosives "ID": "MiningWithExplosives", - "Defaults": { "NexusID": 770 } + "UpdateKeys": [ "Nexus:770" ] }, { // Modder Serialization Utility @@ -1053,7 +1053,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // More Artifact Spots "ID": "451", - "Defaults": { "NexusID": 451 }, + "UpdateKeys": [ "Nexus:451" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -1062,12 +1062,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // More Map Layers "ID": "Platonymous.MoreMapLayers", - "Defaults": { "NexusID": 1134 } + "UpdateKeys": [ "Nexus:1134" ] }, { // More Pets "ID": "821ce8f6-e629-41ad-9fde-03b54f68b0b6MOREPETS | Entoarox.MorePets", // changed in 1.3 - // "Defaults": { "ChucklefishID": 4288 }, // Entoarox opted out of mod update checks + // "UpdateKeys": [ "Chucklefish:4288" ], // Entoarox opted out of mod update checks "Compatibility": { "~1.3.2": { "Status": "AssumeBroken" } // overhauled for SMAPI 1.11+ compatibility } @@ -1075,7 +1075,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // More Rain "ID": "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'More_Rain'} | Omegasis.MoreRain", // changed in 1.5; disambiguate from other mods by Alpha_Omegasis - "Defaults": { "NexusID": 441 }, + "UpdateKeys": [ "Nexus:441" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -1084,17 +1084,17 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // More Weapons "ID": "Joco80.MoreWeapons", - "Defaults": { "NexusID": 1168 } + "UpdateKeys": [ "Nexus:1168" ] }, { // Move Faster "ID": "shuaiz.MoveFasterMod", - "Defaults": { "NexusID": 1351 } + "UpdateKeys": [ "Nexus:1351" ] }, { // Multiple Sprites and Portraits On Rotation (File Loading) "ID": "FileLoading", - "Defaults": { "NexusID": 1094 }, + "UpdateKeys": [ "Nexus:1094" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.12": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -1106,7 +1106,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Museum Rearranger "ID": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Museum Rearranger'} | Omegasis.MuseumRearranger", // changed in 1.4; disambiguate from other mods by Alpha_Omegasis - "Defaults": { "NexusID": 428 }, + "UpdateKeys": [ "Nexus:428" ], "Compatibility": { "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -1114,7 +1114,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // New Machines "ID": "F70D4FAB-0AB2-4B78-9F1B-AF2CA2236A59", - "Defaults": { "ChucklefishID": 3683 }, + "UpdateKeys": [ "Chucklefish:3683" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~4.2.1343": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -1122,7 +1122,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, { // Night Owl - "Defaults": { "NexusID": 433 }, + "UpdateKeys": [ "Nexus:433" ], "ID": "{ID:'SaveAnywhere', Name:'Stardew_NightOwl'} | Omegasis.NightOwl", // changed in 1.4; disambiguate from Save Anywhere "Compatibility": { "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -1144,17 +1144,17 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // No Fence Decay "ID": "cat.nofencedecay", - "Defaults": { "NexusID": 1180 } + "UpdateKeys": [ "Nexus:1180" ] }, { // No More Pets "ID": "NoMorePets | Omegasis.NoMorePets", // changed in 1.4 - "Defaults": { "NexusID": 506 } + "UpdateKeys": [ "Nexus:506" ] }, { // NoSoilDecay "ID": "289dee03-5f38-4d8e-8ffc-e440198e8610", - "Defaults": { "NexusID": 237 }, + "UpdateKeys": [ "Nexus:237" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~0.5": { "Status": "AssumeBroken" } // broke in SDV 1.2, and uses Assembly.GetExecutingAssembly().Location @@ -1163,12 +1163,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // No Soil Decay Redux "ID": "Platonymous.NoSoilDecayRedux", - "Defaults": { "NexusID": 1084 } + "UpdateKeys": [ "Nexus:1084" ] }, { // NPC Map Locations "ID": "NPCMapLocationsMod", - "Defaults": { "NexusID": 239 }, + "UpdateKeys": [ "Nexus:239" ], "Compatibility": { "1.42~1.43": { "Status": "AssumeBroken", @@ -1179,7 +1179,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // NPC Speak "ID": "NpcEcho.dll", - "Defaults": { "NexusID": 694 }, + "UpdateKeys": [ "Nexus:694" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -1188,12 +1188,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Object Time Left "ID": "spacechase0.ObjectTimeLeft", - "Defaults": { "NexusID": 1315 } + "UpdateKeys": [ "Nexus:1315" ] }, { // OmniFarm "ID": "BlueMod_OmniFarm", - "Defaults": { "GitHubProject": "lambui/StardewValleyMod_OmniFarm" }, + "UpdateKeys": [ "GitHub:lambui/StardewValleyMod_OmniFarm" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~2.0.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -1202,7 +1202,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Part of the Community "ID": "SB_PotC", - "Defaults": { "NexusID": 923 }, + "UpdateKeys": [ "Nexus:923" ], "Compatibility": { "~1.0.8": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -1210,7 +1210,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // PelicanFiber "ID": "PelicanFiber.dll | jwdred.PelicanFiber", // changed in 3.0.1 - "Defaults": { "NexusID": 631 }, + "UpdateKeys": [ "Nexus:631" ], "Compatibility": { "~3.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 }, @@ -1221,7 +1221,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // PelicanTTS "ID": "Platonymous.PelicanTTS", - "Defaults": { "NexusID": 1079 }, + "UpdateKeys": [ "Nexus:1079" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.6": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -1230,7 +1230,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Persival's BundleMod "ID": "BundleMod.dll", - "Defaults": { "NexusID": 438 }, + "UpdateKeys": [ "Nexus:438" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.1 @@ -1239,12 +1239,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Plant on Grass "ID": "Demiacle.PlantOnGrass", - "Defaults": { "NexusID": 1026 } + "UpdateKeys": [ "Nexus:1026" ] }, { // Point-and-Plant "ID": "PointAndPlant.dll | jwdred.PointAndPlant", // changed in 1.0.3 - "Defaults": { "NexusID": 572 }, + "UpdateKeys": [ "Nexus:572" ], "Compatibility": { "~1.0.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -1252,17 +1252,17 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Pony Weight Loss Program "ID": "BadNetCode.PonyWeightLossProgram", - "Defaults": { "NexusID": 1232 } + "UpdateKeys": [ "Nexus:1232" ] }, { // Portraiture "ID": "Platonymous.Portraiture", - "Defaults": { "NexusID": 999 } + "UpdateKeys": [ "Nexus:999" ] }, { // Prairie King Made Easy "ID": "PrairieKingMadeEasy.dll | Mucchan.PrairieKingMadeEasy", // changed in 1.0.1 - "Defaults": { "ChucklefishID": 3594 }, + "UpdateKeys": [ "Chucklefish:3594" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.2 @@ -1271,7 +1271,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Quest Delay "ID": "BadNetCode.QuestDelay", - "Defaults": { "NexusID": 1239 } + "UpdateKeys": [ "Nexus:1239" ] }, { // Rain Randomizer @@ -1284,22 +1284,22 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Recatch Legendary Fish "ID": "b3af8c31-48f0-43cf-8343-3eb08bcfa1f9 | community.RecatchLegendaryFish", // changed in 1.3 - "Defaults": { "NexusID": 172 } + "UpdateKeys": [ "Nexus:172" ] }, { // Regeneration "ID": "HammurabiRegeneration", - "Defaults": { "ChucklefishID": 4584 } + "UpdateKeys": [ "Chucklefish:4584" ] }, { // Relationship Bar UI "ID": "RelationshipBar", - "Defaults": { "NexusID": 1009 } + "UpdateKeys": [ "Nexus:1009" ] }, { // RelationshipsEnhanced "ID": "relationshipsenhanced", - "Defaults": { "ChucklefishID": 4435 }, + "UpdateKeys": [ "Chucklefish:4435" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -1308,7 +1308,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Relationship Status "ID": "relationshipstatus", - "Defaults": { "NexusID": 751 }, + "UpdateKeys": [ "Nexus:751" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0.5": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -1320,12 +1320,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Rented Tools "ID": "JarvieK.RentedTools", - "Defaults": { "NexusID": 1307 } + "UpdateKeys": [ "Nexus:1307" ] }, { // Replanter "ID": "Replanter.dll | jwdred.Replanter", // changed in 1.0.5 - "Defaults": { "NexusID": 589 }, + "UpdateKeys": [ "Nexus:589" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -1334,7 +1334,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // ReRegeneration "ID": "lrsk_sdvm_rerg.0925160827", - "Defaults": { "ChucklefishID": 4465 }, + "UpdateKeys": [ "Chucklefish:4465" ], "MapLocalVersions": { "1.1.2-release": "1.1.2" } @@ -1342,12 +1342,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Reseed "ID": "Roc.Reseed", - "Defaults": { "NexusID": 887 } + "UpdateKeys": [ "Nexus:887" ] }, { // Reusable Wallpapers and Floors (Wallpaper Retain) "ID": "dae1b553-2e39-43e7-8400-c7c5c836134b", - "Defaults": { "NexusID": 356 }, + "UpdateKeys": [ "Nexus:356" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.5": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -1356,22 +1356,22 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Ring of Fire "ID": "Platonymous.RingOfFire", - "Defaults": { "NexusID": 1166 } + "UpdateKeys": [ "Nexus:1166" ] }, { // Rope Bridge "ID": "RopeBridge", - "Defaults": { "NexusID": 824 } + "UpdateKeys": [ "Nexus:824" ] }, { // Rotate Toolbar "ID": "Pathoschild.RotateToolbar", - "Defaults": { "NexusID": 1100 } + "UpdateKeys": [ "Nexus:1100" ] }, { // Rush Orders "ID": "RushOrders | spacechase0.RushOrders", // changed in 1.1 - "Defaults": { "NexusID": 605 }, + "UpdateKeys": [ "Nexus:605" ], "Compatibility": { "~1.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -1379,7 +1379,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Save Anywhere "ID": "{ID:'SaveAnywhere', Name:'Save Anywhere'} | Omegasis.SaveAnywhere", // changed in 2.5; disambiguate from Night Owl - "Defaults": { "NexusID": 444 }, + "UpdateKeys": [ "Nexus:444" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~2.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -1391,7 +1391,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Save Backup "ID": "{ID:'4be88c18-b6f3-49b0-ba96-f94b1a5be890', Name:'Stardew_Save_Backup'} | Omegasis.SaveBackup", // changed in 1.3; disambiguate from other Alpha_Omegasis mods - "Defaults": { "NexusID": 435 }, + "UpdateKeys": [ "Nexus:435" ], "Compatibility": { "~1.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -1399,17 +1399,17 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Scroll to Blank "ID": "caraxian.scroll.to.blank", - "Defaults": { "ChucklefishID": 4405 } + "UpdateKeys": [ "Chucklefish:4405" ] }, { // Scythe Harvesting "ID": "ScytheHarvesting | mmanlapat.ScytheHarvesting", // changed in 1.6 - "Defaults": { "NexusID": 1106 } + "UpdateKeys": [ "Nexus:1106" ] }, { // Seasonal Immersion "ID": "EntoaroxSeasonalHouse | EntoaroxSeasonalBuildings | EntoaroxSeasonalImmersion | Entoarox.SeasonalImmersion", // changed in 1.1, 1.6 or earlier, and 1.7 - // "Defaults": { "ChucklefishID": 4262 }, // Entoarox opted out of mod update checks + // "UpdateKeys": [ "Chucklefish:4262" ], // Entoarox opted out of mod update checks "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.8.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -1418,12 +1418,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Seed Bag "ID": "Platonymous.SeedBag", - "Defaults": { "NexusID": 1133 } + "UpdateKeys": [ "Nexus:1133" ] }, { // Self Service "ID": "JarvieK.SelfService", - "Defaults": { "NexusID": 1304 }, + "UpdateKeys": [ "Nexus:1304" ], "MapRemoteVersions": { "0.2.1": "0.2" // manifest not updated } @@ -1431,7 +1431,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Send Items "ID": "Denifia.SendItems", - "Defaults": { "NexusID": 1087 }, + "UpdateKeys": [ "Nexus:1087" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -1440,7 +1440,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Shed Notifications (BuildingsNotifications) "ID": "TheCroak.BuildingsNotifications", - "Defaults": { "NexusID": 620 }, + "UpdateKeys": [ "Nexus:620" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~0.4.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -1449,7 +1449,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Shenandoah Project "ID": "Shenandoah Project", - "Defaults": { "NexusID": 756 }, + "UpdateKeys": [ "Nexus:756" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -1461,12 +1461,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Ship Anywhere "ID": "spacechase0.ShipAnywhere", - "Defaults": { "NexusID": 1379 } + "UpdateKeys": [ "Nexus:1379" ] }, { // Shipment Tracker "ID": "7e474181-e1a0-40f9-9c11-d08a3dcefaf3", - "Defaults": { "NexusID": 321 }, + "UpdateKeys": [ "Nexus:321" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -1475,7 +1475,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Shop Expander "ID": "{ID:'821ce8f6-e629-41ad-9fde-03b54f68b0b6', Name:'Shop Expander'} | EntoaroxShopExpander | Entoarox.ShopExpander", // changed in 1.5 and 1.5.2; disambiguate from Faster Paths - // "Defaults": { "ChucklefishID": 4381 }, // Entoarox opted out of mod update checks + // "UpdateKeys": [ "Chucklefish:4381" ], // Entoarox opted out of mod update checks "Compatibility": { "~1.5.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -1483,7 +1483,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Showcase Mod "ID": "Igorious.Showcase", - "Defaults": { "ChucklefishID": 4487 }, + "UpdateKeys": [ "Chucklefish:4487" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~0.9": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -1495,22 +1495,22 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Shroom Spotter "ID": "TehPers.ShroomSpotter", - "Defaults": { "NexusID": 908 } + "UpdateKeys": [ "Nexus:908" ] }, { // Simple Crop Label "ID": "SimpleCropLabel", - "Defaults": { "NexusID": 314 } + "UpdateKeys": [ "Nexus:314" ] }, { // Simple Sound Manager "ID": "Omegasis.SimpleSoundManager", - "Defaults": { "NexusID": 1410 } + "UpdateKeys": [ "Nexus:1410" ] }, { // Simple Sprinklers "ID": "SimpleSprinkler.dll | tZed.SimpleSprinkler", // changed in 1.5 - "Defaults": { "NexusID": 76 }, + "UpdateKeys": [ "Nexus:76" ], "Compatibility": { "~1.4": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -1518,7 +1518,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Siv's Marriage Mod "ID": "6266959802", - "Defaults": { "NexusID": 366 }, + "UpdateKeys": [ "Nexus:366" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.2.2": { "Status": "AssumeBroken" } // broke in SMAPI 1.9 (has multiple Mod instances) @@ -1530,7 +1530,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Skill Prestige "ID": "6b843e60-c8fc-4a25-a67b-4a38ac8dcf9b | alphablackwolf.skillPrestige", // changed circa 1.2.3 - "Defaults": { "NexusID": 569 }, + "UpdateKeys": [ "Nexus:569" ], "Compatibility": { "~1.0.9": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -1538,7 +1538,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Skill Prestige: Cooking Adapter "ID": "20d6b8a3-b6e7-460b-a6e4-07c2b0cb6c63 | Alphablackwolf.CookingSkillPrestigeAdapter", // changed circa 1.1 - "Defaults": { "NexusID": 569 }, + "UpdateKeys": [ "Nexus:569" ], "Compatibility": { "~1.0.9": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 }, @@ -1549,27 +1549,27 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Skip Intro "ID": "SkipIntro | Pathoschild.SkipIntro", // changed in 1.4 - "Defaults": { "NexusID": 533 } + "UpdateKeys": [ "Nexus:533" ] }, { // Skull Cavern Elevator "ID": "SkullCavernElevator", - "Defaults": { "NexusID": 963 } + "UpdateKeys": [ "Nexus:963" ] }, { // Skull Cave Saver "ID": "8ac06349-26f7-4394-806c-95d48fd35774 | community.SkullCaveSaver", // changed in 1.1 - "Defaults": { "NexusID": 175 } + "UpdateKeys": [ "Nexus:175" ] }, { // Sleepy Eye "ID": "spacechase0.SleepyEye", - "Defaults": { "NexusID": 1152 } + "UpdateKeys": [ "Nexus:1152" ] }, { // Slower Fence Decay "ID": "SPDSlowFenceDecay", - "Defaults": { "NexusID": 252 }, + "UpdateKeys": [ "Nexus:252" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~0.5.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -1586,7 +1586,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Solar Eclipse Event "ID": "KoihimeNakamura.SolarEclipseEvent", - "Defaults": { "NexusID": 897 }, + "UpdateKeys": [ "Nexus:897" ], "Compatibility": { "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 }, @@ -1597,17 +1597,17 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // SpaceCore "ID": "spacechase0.SpaceCore", - "Defaults": { "NexusID": 1348 } + "UpdateKeys": [ "Nexus:1348" ] }, { // Speedster "ID": "Platonymous.Speedster", - "Defaults": { "NexusID": 1102 } + "UpdateKeys": [ "Nexus:1102" ] }, { // Sprinkler Range "ID": "cat.sprinklerrange", - "Defaults": { "NexusID": 1179 }, + "UpdateKeys": [ "Nexus:1179" ], "MapRemoteVersions": { "1.0.1": "1.0" // manifest not updated } @@ -1615,7 +1615,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Sprinkles "ID": "Platonymous.Sprinkles", - "Defaults": { "ChucklefishID": 4592 }, + "UpdateKeys": [ "Chucklefish:4592" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -1624,7 +1624,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Sprint and Dash "ID": "SPDSprintAndDash", - "Defaults": { "ChucklefishID": 3531 }, + "UpdateKeys": [ "Chucklefish:3531" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.2 @@ -1633,12 +1633,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Sprint and Dash Redux "ID": "lrsk_sdvm_sndr.0921161059 | littleraskol.SprintAndDashRedux", // changed in 1.3 - "Defaults": { "ChucklefishID": 4201 } + "UpdateKeys": [ "Chucklefish:4201" ] }, { // Sprinting Mod "ID": "a10d3097-b073-4185-98ba-76b586cba00c", - "Defaults": { "GitHubProject": "oliverpl/SprintingMod" }, + "UpdateKeys": [ "GitHub:oliverpl/SprintingMod" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~2.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 @@ -1650,7 +1650,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // StackSplitX "ID": "StackSplitX.dll | tstaples.StackSplitX", // changed circa 1.3.1 - "Defaults": { "NexusID": 798 }, + "UpdateKeys": [ "Nexus:798" ], "Compatibility": { "~1.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -1666,12 +1666,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Stardew Config Menu "ID": "Juice805.StardewConfigMenu", - "Defaults": { "NexusID": 1312 } + "UpdateKeys": [ "Nexus:1312" ] }, { // Stardew Content Compatibility Layer (SCCL) "ID": "SCCL", - "Defaults": { "NexusID": 889 }, + "UpdateKeys": [ "Nexus:889" ], "Compatibility": { "~0.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -1679,12 +1679,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Stardew Editor Game Integration "ID": "spacechase0.StardewEditor.GameIntegration", - "Defaults": { "NexusID": 1298 } + "UpdateKeys": [ "Nexus:1298" ] }, { // Stardew Notification "ID": "stardewnotification", - "Defaults": { "GitHubProject": "monopandora/StardewNotification" }, + "UpdateKeys": [ "GitHub:monopandora/StardewNotification" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.7": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -1693,7 +1693,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Stardew Symphony "ID": "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'Stardew_Symphony'} | Omegasis.StardewSymphony", // changed in 1.4; disambiguate other mods by Alpha_Omegasis - "Defaults": { "NexusID": 425 }, + "UpdateKeys": [ "Nexus:425" ], "Compatibility": { "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -1711,12 +1711,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Starting Money "ID": "StartingMoney | mmanlapat.StartingMoney", // changed in 1.1 - "Defaults": { "NexusID": 1138 } + "UpdateKeys": [ "Nexus:1138" ] }, { // StashItemsToChest "ID": "BlueMod_StashItemsToChest", - "Defaults": { "GitHubProject": "lambui/StardewValleyMod_StashItemsToChest" }, + "UpdateKeys": [ "GitHub:lambui/StardewValleyMod_StashItemsToChest" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -1725,7 +1725,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Stephan's Lots of Crops "ID": "stephansstardewcrops", - "Defaults": { "ChucklefishID": 4314 }, + "UpdateKeys": [ "Chucklefish:4314" ], "MapRemoteVersions": { "1.41": "1.1" // manifest not updated } @@ -1733,7 +1733,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Stone Bridge Over Pond (PondWithBridge) "ID": "PondWithBridge.dll", - "Defaults": { "NexusID": 316 }, + "UpdateKeys": [ "Nexus:316" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -1745,12 +1745,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Stumps to Hardwood Stumps "ID": "StumpsToHardwoodStumps", - "Defaults": { "NexusID": 691 } + "UpdateKeys": [ "Nexus:691" ] }, { // Super Greenhouse Warp Modifier "ID": "SuperGreenhouse", - "Defaults": { "ChucklefishID": 4334 }, + "UpdateKeys": [ "Chucklefish:4334" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -1759,7 +1759,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Swim Almost Anywhere / Swim Suit "ID": "Platonymous.SwimSuit", - "Defaults": { "NexusID": 1215 } + "UpdateKeys": [ "Nexus:1215" ] }, { // Tainted Cellar @@ -1772,17 +1772,17 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Tapper Ready "ID": "skunkkk.TapperReady", - "Defaults": { "NexusID": 1219 } + "UpdateKeys": [ "Nexus:1219" ] }, { // Teh's Fishing Overhaul "ID": "TehPers.FishingOverhaul", - "Defaults": { "NexusID": 866 } + "UpdateKeys": [ "Nexus:866" ] }, { // Teleporter "ID": "Teleporter", - "Defaults": { "ChucklefishID": 4374 }, + "UpdateKeys": [ "Chucklefish:4374" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 @@ -1791,7 +1791,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Three-heart Dance Partner "ID": "ThreeHeartDancePartner", - "Defaults": { "NexusID": 500 }, + "UpdateKeys": [ "Nexus:500" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 @@ -1800,12 +1800,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // TimeFreeze "ID": "4108e859-333c-4fec-a1a7-d2e18c1019fe", - "Defaults": { "NexusID": 973 } + "UpdateKeys": [ "Nexus:973" ] }, { // Time Reminder "ID": "KoihimeNakamura.TimeReminder", - "Defaults": { "NexusID": 1000 }, + "UpdateKeys": [ "Nexus:1000" ], "MapLocalVersions": { "1.0-20170314": "1.0.2" } @@ -1813,7 +1813,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // TimeSpeed "ID": "TimeSpeed.dll | {ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'TimeSpeed'} | {ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'TimeSpeed Mod (unofficial)'} | community.TimeSpeed", // changed in 2.0.3 and 2.1; disambiguate other mods by Alpha_Omegasis - "Defaults": { "NexusID": 169 }, + "UpdateKeys": [ "Nexus:169" ], "Compatibility": { "~2.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -1821,22 +1821,22 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // TractorMod "ID": "BlueMod_TractorMod | PhthaloBlue.TractorMod | community.TractorMod | Pathoschild.TractorMod", // changed in 3.2, 4.0 beta, and 4.0 - "Defaults": { "NexusID": 1401 } + "UpdateKeys": [ "Nexus:1401" ] }, { // Tree Transplant "ID": "TreeTransplant", - "Defaults": { "NexusID": 1342 } + "UpdateKeys": [ "Nexus:1342" ] }, { // UI Info Suite "ID": "Cdaragorn.UiInfoSuite", - "Defaults": { "NexusID": 1150 } + "UpdateKeys": [ "Nexus:1150" ] }, { // UiModSuite "ID": "Demiacle.UiModSuite", - "Defaults": { "NexusID": 1023 }, + "UpdateKeys": [ "Nexus:1023" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0": { "Status": "AssumeBroken" } // broke in SDV 1.2 @@ -1848,12 +1848,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Variable Grass "ID": "dantheman999.VariableGrass", - "Defaults": { "GitHubProject": "dantheman999301/StardewMods" } + "UpdateKeys": [ "GitHub:dantheman999301/StardewMods" ] }, { // Vertical Toolbar "ID": "SB_VerticalToolMenu", - "Defaults": { "NexusID": 943 } + "UpdateKeys": [ "Nexus:943" ] }, { // WakeUp @@ -1866,7 +1866,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Wallpaper Fix "ID": "WallpaperFix.dll", - "Defaults": { "ChucklefishID": 4211 }, + "UpdateKeys": [ "Chucklefish:4211" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -1875,7 +1875,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // WarpAnimals "ID": "Symen.WarpAnimals", - "Defaults": { "NexusID": 1400 } + "UpdateKeys": [ "Nexus:1400" ] }, { // Weather Controller @@ -1888,12 +1888,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // What Farm Cave / WhatAMush "ID": "WhatAMush", - "Defaults": { "NexusID": 1097 } + "UpdateKeys": [ "Nexus:1097" ] }, { // WHats Up "ID": "wHatsUp", - "Defaults": { "NexusID": 1082 } + "UpdateKeys": [ "Nexus:1082" ] }, { // Wonderful Farm Life @@ -1916,7 +1916,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Xnb Loader "ID": "Entoarox.XnbLoader", - // "Defaults": { "ChucklefishID": 4506 }, // Entoarox opted out of mod update checks + // "UpdateKeys": [ "Chucklefish:4506" ], // Entoarox opted out of mod update checks "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0.6": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 @@ -1925,7 +1925,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // zDailyIncrease "ID": "zdailyincrease", - "Defaults": { "ChucklefishID": 4247 }, + "UpdateKeys": [ "Chucklefish:4247" ], "Compatibility": { "~1.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 }, @@ -1936,7 +1936,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Zoom Out Extreme "ID": "ZoomMod | RockinMods.ZoomMod", // changed circa 1.2.1 - "Defaults": { "NexusID": 1326 }, + "UpdateKeys": [ "Nexus:1326" ], "Compatibility": { "~0.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -1944,7 +1944,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Zoryn's Better RNG "ID": "76b6d1e1-f7ba-4d72-8c32-5a1e6d2716f6 | Zoryn.BetterRNG", // changed in 1.6 - "Defaults": { "GitHubProject": "Zoryn4163/SMAPI-Mods" }, + "UpdateKeys": [ "GitHub:Zoryn4163/SMAPI-Mods" ], "Compatibility": { "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -1952,7 +1952,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Zoryn's Calendar Anywhere "ID": "a41c01cd-0437-43eb-944f-78cb5a53002a | Zoryn.CalendarAnywhere", // changed in 1.6 - "Defaults": { "GitHubProject": "Zoryn4163/SMAPI-Mods" }, + "UpdateKeys": [ "GitHub:Zoryn4163/SMAPI-Mods" ], "Compatibility": { "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -1960,12 +1960,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Zoryn's Durable Fences "ID": "56d3439c-7b9b-497e-9496-0c4890e8a00e | Zoryn.DurableFences", // changed in 1.6 - "Defaults": { "GitHubProject": "Zoryn4163/SMAPI-Mods" } + "UpdateKeys": [ "GitHub:Zoryn4163/SMAPI-Mods" ] }, { // Zoryn's Health Bars "ID": "HealthBars.dll | Zoryn.HealthBars", // changed in 1.6 - "Defaults": { "GitHubProject": "Zoryn4163/SMAPI-Mods" }, + "UpdateKeys": [ "GitHub:Zoryn4163/SMAPI-Mods" ], "Compatibility": { "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -1973,12 +1973,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Zoryn's Fishing Mod "ID": "fa277b1f-265e-47c3-a84f-cd320cc74949 | Zoryn.FishingMod", // changed in 1.6 - "Defaults": { "GitHubProject": "Zoryn4163/SMAPI-Mods" } + "UpdateKeys": [ "GitHub:Zoryn4163/SMAPI-Mods" ] }, { // Zoryn's Junimo Deposit Anywhere "ID": "f93a4fe8-cade-4146-9335-b5f82fbbf7bc | Zoryn.JunimoDepositAnywhere", // changed in 1.6 - "Defaults": { "GitHubProject": "Zoryn4163/SMAPI-Mods" }, + "UpdateKeys": [ "GitHub:Zoryn4163/SMAPI-Mods" ], "Compatibility": { "~1.7": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -1986,7 +1986,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Zoryn's Movement Mod "ID": "8a632929-8335-484f-87dd-c29d2ba3215d | Zoryn.MovementModifier", // changed in 1.6 - "Defaults": { "GitHubProject": "Zoryn4163/SMAPI-Mods" }, + "UpdateKeys": [ "GitHub:Zoryn4163/SMAPI-Mods" ], "Compatibility": { "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.2 } @@ -1994,7 +1994,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Zoryn's Regen Mod "ID": "dfac4383-1b6b-4f33-ae4e-37fc23e5252e | Zoryn.RegenMod", // changed in 1.6 - "Defaults": { "GitHubProject": "Zoryn4163/SMAPI-Mods" }, + "UpdateKeys": [ "GitHub:Zoryn4163/SMAPI-Mods" ], "Compatibility": { "~1.6": { "Status": "AssumeBroken" } // broke in SDV 1.2 } diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 5bf46ac6..8863590b 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -91,7 +91,6 @@ Properties\GlobalAssemblyInfo.cs - From 2c87961c9e47aeba6eb7dbfca234cc457cb02158 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 25 Sep 2017 21:21:27 -0400 Subject: [PATCH 119/186] improve mod update-check validation & errors (#336) --- .../Controllers/ModsController.cs | 4 ++-- .../Framework/ModRepositories/ChucklefishRepository.cs | 5 +++++ .../Framework/ModRepositories/GitHubRepository.cs | 10 ++++++++++ .../Framework/ModRepositories/NexusRepository.cs | 5 +++++ 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/StardewModdingAPI.Web/Controllers/ModsController.cs b/src/StardewModdingAPI.Web/Controllers/ModsController.cs index 26cdfa31..7dcfcf13 100644 --- a/src/StardewModdingAPI.Web/Controllers/ModsController.cs +++ b/src/StardewModdingAPI.Web/Controllers/ModsController.cs @@ -106,14 +106,14 @@ namespace StardewModdingAPI.Web.Controllers // parse mod key if (!this.TryParseModKey(modKey, out string vendorKey, out string modID)) { - result[modKey] = new ModInfoModel("The mod key isn't in a valid format. It should contain the mod repository key and mod ID like 'Nexus:541'."); + result[modKey] = new ModInfoModel("The mod key isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541'."); continue; } // get matching repository if (!this.Repositories.TryGetValue(vendorKey, out IModRepository repository)) { - result[modKey] = new ModInfoModel("There's no mod repository matching this namespaced mod ID."); + result[modKey] = new ModInfoModel($"There's no mod site with key '{vendorKey}'. Expected one of [{string.Join(", ", this.Repositories.Keys)}]."); continue; } diff --git a/src/StardewModdingAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs b/src/StardewModdingAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs index 4822c414..ed7bd60b 100644 --- a/src/StardewModdingAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs +++ b/src/StardewModdingAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs @@ -43,6 +43,11 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories /// The mod ID in this repository. public override async Task GetModInfoAsync(string id) { + // validate ID format + if (!uint.TryParse(id, out uint _)) + return new ModInfoModel($"The value '{id}' isn't a valid Chucklefish mod ID, must be an integer ID."); + + // fetch info try { // fetch HTML diff --git a/src/StardewModdingAPI.Web/Framework/ModRepositories/GitHubRepository.cs b/src/StardewModdingAPI.Web/Framework/ModRepositories/GitHubRepository.cs index 7dfe9f62..174fb79a 100644 --- a/src/StardewModdingAPI.Web/Framework/ModRepositories/GitHubRepository.cs +++ b/src/StardewModdingAPI.Web/Framework/ModRepositories/GitHubRepository.cs @@ -1,4 +1,5 @@ using System; +using System.Net; using System.Threading.Tasks; using Newtonsoft.Json; using Pathoschild.Http.Client; @@ -46,6 +47,11 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories /// The mod ID in this repository. public override async Task GetModInfoAsync(string id) { + // validate ID format + if (!id.Contains("/") || id.IndexOf("/", StringComparison.InvariantCultureIgnoreCase) != id.LastIndexOf("/", StringComparison.InvariantCultureIgnoreCase)) + return new ModInfoModel($"The value '{id}' isn't a valid GitHub mod ID, must be a username and project name like 'Pathoschild/LookupAnything'."); + + // fetch info try { GitRelease release = await this.Client @@ -53,6 +59,10 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories .As(); return new ModInfoModel(id, this.NormaliseVersion(release.Tag), $"https://github.com/{id}/releases"); } + catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound) + { + return new ModInfoModel("Found no mod with this ID."); + } catch (Exception ex) { return new ModInfoModel(ex.ToString()); diff --git a/src/StardewModdingAPI.Web/Framework/ModRepositories/NexusRepository.cs b/src/StardewModdingAPI.Web/Framework/ModRepositories/NexusRepository.cs index e679b977..71970bec 100644 --- a/src/StardewModdingAPI.Web/Framework/ModRepositories/NexusRepository.cs +++ b/src/StardewModdingAPI.Web/Framework/ModRepositories/NexusRepository.cs @@ -38,6 +38,11 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories /// The mod ID in this repository. public override async Task GetModInfoAsync(string id) { + // validate ID format + if (!uint.TryParse(id, out uint _)) + return new ModInfoModel($"The value '{id}' isn't a valid Nexus mod ID, must be an integer ID."); + + // fetch info try { NexusResponseModel response = await this.Client From 4aa028dc74d3d92ce141e0f4a98f54690ecf97a5 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 25 Sep 2017 21:50:15 -0400 Subject: [PATCH 120/186] polish release notes --- release-notes.md | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/release-notes.md b/release-notes.md index a1473313..4f4667d9 100644 --- a/release-notes.md +++ b/release-notes.md @@ -3,25 +3,26 @@ For players: -* The console is now simpler and easier to read. -* The console now adjusts its colors when you have a light terminal background. -* SMAPI now detects mods which may impact game stability and shows a warning in the console. -* SMAPI now alerts you in the console when one of your mods has a new version. - _That includes most existing mods, even if they haven't updated to use the new update-check API yet._ -* Renamed installer folder from `SMAPI 2.0` to `SMAPI 2.0 installer` to avoid confusion. +* SMAPI now alerts you when mods have new versions available. +* SMAPI now warns you about mods which may impact game stability. +* The console is now simpler and easier to read, and adjusts its colors to fit your terminal background color. +* Renamed installer folder to avoid confusion. * Updated compatibility list. * Fixed update check errors on Linux/Mac. For mod developers: -* Added new APIs to edit, inject, and reload XNB assets loaded by the game at any time. - _This let mods do anything previously only possible with XNB mods, plus enables new mod scenarios (e.g. seasonal textures, NPC clothing that depend on the weather or location, etc)._ -* Added support for automatic update checks from Chucklefish, GitHub, or Nexus Mods. -* Added new input events. - _The new `InputEvents` combine keyboard + mouse + controller input into one event for easy handling, add metadata like the cursor position and grab tile to support click handling, and add an option to suppress input from the game to enable new scenarios like action highjacking and UI overlays._ +* Added support for editing, injecting, and reloading XNB data loaded by the game at any time. + _This let SMAPI mods do anything previously only possible with XNB mods, and enables new mod scenarios not possible with XNB mods (e.g. seasonal textures, NPC clothing that depend on the weather or location, etc)._ +* Added support for automatic mod update checks. + _Add your Chucklefish, GitHub, or Nexus mod ID to the manifest and SMAPI will check for updates automatically. SMAPI will retroactively enable updates for most existing mods._ +* Added unified input events. + _The new `InputEvents` combine keyboard + mouse + controller input into one event and constant for easy handling, add metadata like the cursor position and grab tile to support click handling._ +* Added support for suppressing input. + _You can prevent the game from receiving input via `InputEvents`, to enable new scenarios like action highjacking and UI overlays._ * Added support for optional dependencies. -* Added support for string versions (like `"1.0-alpha"`) in `manifest.json`. -* Added `IEquatable` to `ISemanticVersion`. +* Added support for specifying the mod version as a string (like `"1.0-alpha"`) in `manifest.json`. * Added day of week to `SDate` instances. +* Added `IEquatable` to `ISemanticVersion`. * Removed the TrainerMod's `save` and `load` commands. * Removed all deprecated code. * Removed support for mods with no `Name`, `Version`, or `UniqueID` in their manifest. @@ -33,9 +34,9 @@ For power users: * Added command-line arguments to the SMAPI installer so it can be scripted. For SMAPI developers: -* SMAPI has been significantly refactored under the hood to support changes in 2.0 and upcoming releases. -* Overhauled `StardewModdingAPI.config.json` format to support injecting mod data. -* Removed SMAPI 1._x_ compatibility code. +* Significantly refactored SMAPI to support changes in 2.0 and upcoming releases. +* Overhauled `StardewModdingAPI.config.json` format to support mod data like update keys. +* Removed SMAPI 1._x_ compatibility mode. ## 1.15.4 For players: From 07382277eacdb91a3994bca35fed40a060fbcf0c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 25 Sep 2017 22:15:30 -0400 Subject: [PATCH 121/186] add support for multiple mods having the same update key (#336) --- src/StardewModdingAPI/Program.cs | 48 +++++++++++++++++++------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 3575add6..0b58756e 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -517,38 +517,46 @@ namespace StardewModdingAPI // check mod versions try { - // prepare update keys - this.VerboseLog("Collecting mod update keys..."); - IDictionary modsByKey = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - foreach (IModMetadata mod in mods) + // log issues + if (this.Settings.VerboseLogging) { - // validate - if (mod.Manifest == null) + this.VerboseLog("Validating mod update keys..."); + foreach (IModMetadata mod in mods) { - this.VerboseLog($" {mod.DisplayName}: no manifest."); - continue; + if (mod.Manifest == null) + this.VerboseLog($" {mod.DisplayName}: no manifest."); + else if (mod.Manifest.UpdateKeys == null || !mod.Manifest.UpdateKeys.Any()) + this.VerboseLog($" {mod.DisplayName}: no update keys."); } - - // add update keys - if (mod.Manifest.UpdateKeys?.Any() == true) - { - foreach (string updateKey in mod.Manifest.UpdateKeys) - modsByKey[updateKey] = mod; - } - else - this.VerboseLog($" {mod.DisplayName}: no update keys."); } + // prepare update keys + Dictionary modsByKey = + ( + from mod in mods + where mod.Manifest?.UpdateKeys != null + from key in mod.Manifest.UpdateKeys + select new { key, mod } + ) + .GroupBy(p => p.key, StringComparer.InvariantCultureIgnoreCase) + .ToDictionary( + group => group.Key, + group => group.Select(p => p.mod).ToArray(), + StringComparer.InvariantCultureIgnoreCase + ); + // fetch results - this.Monitor.Log($"Checking for updates to {modsByKey.Count} keys...", LogLevel.Trace); + this.Monitor.Log($"Checking for updates to {modsByKey.Keys.Count} keys...", LogLevel.Trace); var results = ( from entry in client.GetModInfoAsync(modsByKey.Keys.ToArray()).Result - let mod = modsByKey[entry.Key] + from mod in modsByKey[entry.Key] orderby mod.DisplayName - select new { entry.Key, Mod = modsByKey[entry.Key], Info = entry.Value } + select new { entry.Key, Mod = mod, Info = entry.Value } ) .ToArray(); + + // extract latest versions IDictionary updatesByMod = new Dictionary(); foreach (var result in results) { From c1a9dc7f7e21660db50fd6d1b892f7c3c3dbd673 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 26 Sep 2017 01:55:26 -0400 Subject: [PATCH 122/186] minor cleanup after 1.x removal --- .../Framework/CommandManager.cs | 9 ++++----- .../Framework/DeprecationManager.cs | 18 +----------------- .../Framework/Models/Manifest.cs | 2 +- src/StardewModdingAPI/IManifest.cs | 2 +- src/StardewModdingAPI/Mod.cs | 4 ++-- src/StardewModdingAPI/Program.cs | 8 +++----- 6 files changed, 12 insertions(+), 31 deletions(-) diff --git a/src/StardewModdingAPI/Framework/CommandManager.cs b/src/StardewModdingAPI/Framework/CommandManager.cs index 9af3d27a..79a23d03 100644 --- a/src/StardewModdingAPI/Framework/CommandManager.cs +++ b/src/StardewModdingAPI/Framework/CommandManager.cs @@ -52,8 +52,7 @@ namespace StardewModdingAPI.Framework public Command Get(string name) { name = this.GetNormalisedName(name); - Command command; - this.Commands.TryGetValue(name, out command); + this.Commands.TryGetValue(name, out Command command); return command; } @@ -92,8 +91,7 @@ namespace StardewModdingAPI.Framework return false; // get command - Command command; - if (this.Commands.TryGetValue(name, out command)) + if (this.Commands.TryGetValue(name, out Command command)) { command.Callback.Invoke(name, arguments); return true; @@ -101,6 +99,7 @@ namespace StardewModdingAPI.Framework return false; } + /********* ** Private methods *********/ @@ -114,4 +113,4 @@ namespace StardewModdingAPI.Framework : null; } } -} \ No newline at end of file +} diff --git a/src/StardewModdingAPI/Framework/DeprecationManager.cs b/src/StardewModdingAPI/Framework/DeprecationManager.cs index 43e82d74..b07c6c7d 100644 --- a/src/StardewModdingAPI/Framework/DeprecationManager.cs +++ b/src/StardewModdingAPI/Framework/DeprecationManager.cs @@ -1,6 +1,5 @@ -using System; +using System; using System.Collections.Generic; -using System.Reflection; namespace StardewModdingAPI.Framework { @@ -52,10 +51,6 @@ namespace StardewModdingAPI.Framework if (!this.MarkWarned(source ?? "", nounPhrase, version)) return; - // show SMAPI 2.0 meta-warning - if(this.MarkWarned("SMAPI", "SMAPI 2.0 meta-warning", "2.0")) - this.Monitor.Log("Some mods may stop working in SMAPI 2.0 (but they'll work fine for now). Try updating mods with 'deprecated code' warnings; if that doesn't remove the warnings, let the mod authors know about this message or see http://stardewvalleywiki.com/Modding:SMAPI_2.0 for details.", LogLevel.Warn); - // build message string message = $"{source ?? "An unknown mod"} uses deprecated code ({nounPhrase})."; if (source == null) @@ -106,16 +101,5 @@ namespace StardewModdingAPI.Framework this.LoggedDeprecations.Add(key); return true; } - - /// Get whether a type implements the given virtual method. - /// The type to check. - /// The base type which declares the virtual method. - /// The method name. - /// The expected argument types. - internal bool IsVirtualMethodImplemented(Type subtype, Type baseType, string name, Type[] argumentTypes) - { - MethodInfo method = subtype.GetMethod(nameof(Mod.Entry), argumentTypes); - return method.DeclaringType != baseType; - } } } diff --git a/src/StardewModdingAPI/Framework/Models/Manifest.cs b/src/StardewModdingAPI/Framework/Models/Manifest.cs index a051354c..b85787e5 100644 --- a/src/StardewModdingAPI/Framework/Models/Manifest.cs +++ b/src/StardewModdingAPI/Framework/Models/Manifest.cs @@ -27,7 +27,7 @@ namespace StardewModdingAPI.Framework.Models [JsonConverter(typeof(SFieldConverter))] public ISemanticVersion MinimumApiVersion { get; set; } - /// The name of the DLL in the directory that has the method. + /// The name of the DLL in the directory that has the method. public string EntryDll { get; set; } /// The other mods that must be loaded before this mod. diff --git a/src/StardewModdingAPI/IManifest.cs b/src/StardewModdingAPI/IManifest.cs index befef901..9db1d538 100644 --- a/src/StardewModdingAPI/IManifest.cs +++ b/src/StardewModdingAPI/IManifest.cs @@ -26,7 +26,7 @@ namespace StardewModdingAPI /// The unique mod ID. string UniqueID { get; } - /// The name of the DLL in the directory that has the method. + /// The name of the DLL in the directory that has the method. string EntryDll { get; } /// The other mods that must be loaded before this mod. diff --git a/src/StardewModdingAPI/Mod.cs b/src/StardewModdingAPI/Mod.cs index c511ce5a..ee75ba54 100644 --- a/src/StardewModdingAPI/Mod.cs +++ b/src/StardewModdingAPI/Mod.cs @@ -3,7 +3,7 @@ using System; namespace StardewModdingAPI { /// The base class for a mod. - public class Mod : IMod, IDisposable + public abstract class Mod : IMod, IDisposable { /********* ** Accessors @@ -23,7 +23,7 @@ namespace StardewModdingAPI *********/ /// The mod entry point, called after the mod is first loaded. /// Provides simplified APIs for writing mods. - public virtual void Entry(IModHelper helper) { } + public abstract void Entry(IModHelper helper); /// Release or reset unmanaged resources. public void Dispose() diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 0b58756e..ba8c7847 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -781,8 +781,6 @@ namespace StardewModdingAPI { IMod mod = metadata.Mod; mod.Entry(mod.Helper); - if (!this.DeprecationManager.IsVirtualMethodImplemented(mod.GetType(), typeof(Mod), nameof(Mod.Entry), new[] { typeof(IModHelper) })) - this.Monitor.Log($"{metadata.DisplayName} doesn't implement Entry() and may not work correctly.", LogLevel.Error); } catch (Exception ex) { @@ -816,8 +814,8 @@ namespace StardewModdingAPI } // reset cache now if any editors or loaders were added during entry - IAssetEditor[] editors = loadedMods.SelectMany(p => ((ContentHelper)p.Mod.Helper.Content).AssetEditors).ToArray(); - IAssetLoader[] loaders = loadedMods.SelectMany(p => ((ContentHelper)p.Mod.Helper.Content).AssetLoaders).ToArray(); + IAssetEditor[] editors = loadedMods.SelectMany(p => p.Mod.Helper.Content.AssetEditors).ToArray(); + IAssetLoader[] loaders = loadedMods.SelectMany(p => p.Mod.Helper.Content.AssetLoaders).ToArray(); if (editors.Any() || loaders.Any()) { this.Monitor.Log("Invalidating cached assets for new editors & loaders...", LogLevel.Trace); @@ -866,7 +864,7 @@ namespace StardewModdingAPI case "help": if (arguments.Any()) { - Framework.Command result = this.CommandManager.Get(arguments[0]); + Command result = this.CommandManager.Get(arguments[0]); if (result == null) this.Monitor.Log("There's no command with that name.", LogLevel.Error); else From 525a3efb91919dc327a35862ca20a1ef00fbe3c4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 26 Sep 2017 10:33:43 -0400 Subject: [PATCH 123/186] add data for a few more mods (#361) --- .../StardewModdingAPI.config.json | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/src/StardewModdingAPI/StardewModdingAPI.config.json b/src/StardewModdingAPI/StardewModdingAPI.config.json index 54fe52c5..662254e3 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.config.json +++ b/src/StardewModdingAPI/StardewModdingAPI.config.json @@ -210,6 +210,11 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "1.0.2": "1.0.1-release" // manifest not updated } }, + { + // Better Hay + "ID": "cat.betterhay", + "UpdateKeys": [ "Nexus:1430" ] + }, { // Better Quality More Seasons "ID": "SB_BQMS", @@ -437,6 +442,11 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": "musbah.bundleTooltip", "UpdateKeys": [ "Nexus:1329" ] }, + { + // Concentration on Farming + "ID": "punyo.ConcentrationOnFarming", + "UpdateKeys": [ "Nexus:1445" ] + }, { // Configurable Machines "ID": "21da6619-dc03-4660-9794-8e5b498f5b97", @@ -600,6 +610,11 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": "BashNinja.DynamicNPCSprites", "UpdateKeys": [ "Nexus:1183" ] }, + { + // Easier Farming + "ID": "cautiouswafffle.EasierFarming", + "UpdateKeys": [ "Nexus:1426" ] + }, { // Empty Hands "ID": "QuicksilverFox.EmptyHands", @@ -664,6 +679,11 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": "{ID:'EntoaroxFurnitureAnywhere', Name:'Extended Minecart'} | Entoarox.ExtendedMinecart" // changed in 1.6.1 //"UpdateKeys": [ "Chucklefish:4359" ] // Entoarox opted out of mod update checks }, + { + // Extended Reach + "ID": "spacechase0.ExtendedReach", + "UpdateKeys": [ "Nexus:1493" ] + }, { // Fall 28 Snow Day "ID": "{ID:'7ad4f6f7-c3de-4729-a40f-7a11d2b2a358', Name:'Fall28 Snow Day'} | Omegasis.Fall28SnowDay", // changed in 1.4; disambiguate from other mods by Alpha_Omegasis @@ -796,6 +816,11 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~0.1.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } }, + { + // Geode Info Menu + "ID": "cat.geodeinfomenu", + "UpdateKeys": [ "Nexus:1448" ] + }, { // Get Dressed "ID": "GetDressed.dll | Advize.GetDressed", // changed in 3.3 @@ -956,6 +981,14 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "0.0": "1.1" } }, + { + // Level Extender + "ID": "Devin Lematty.Level Extender", + "UpdateKeys": [ "Nexus:1471" ], + "MapRemoteVersions": { + "1.1": "1.0" // manifest not updated + } + }, { // Level Up Notifications "ID": "Level Up Notifications", @@ -1131,6 +1164,11 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "2.1": "1.3" // 1.3 had wrong version in manifest } }, + { + // No Kids Ever + "ID": "Hangy.NoKidsEver", + "UpdateKeys": [ "Nexus:1464" ] + }, { // No Debug Mode "ID": "NoDebugMode", @@ -1199,6 +1237,11 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~2.0.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } }, + { + // Out of Season Bonuses / Seasonal Items + "ID": "midoriarmstrong.seasonalitems", + "UpdateKeys": [ "Nexus:1452" ] + }, { // Part of the Community "ID": "SB_PotC", @@ -1227,6 +1270,11 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~1.6": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } }, + { + // Persia the Mermaid - Standalone Custom NPC + "ID": "63b9f419-7449-42db-ab2e-440b4d05c073", + "UpdateKeys": [ "Nexus:1419" ] + }, { // Persival's BundleMod "ID": "BundleMod.dll", @@ -1788,6 +1836,11 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~1.0.2": { "Status": "AssumeBroken" } // broke in SDV 1.2 } }, + { + // The Long Night + "ID": "Pathoschild.TheLongNight", + "UpdateKeys": [ "Nexus:1369" ] + }, { // Three-heart Dance Partner "ID": "ThreeHeartDancePartner", From b67c0602c651d0ec973d527dbcfdb5f47ac35624 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 26 Sep 2017 16:45:47 -0400 Subject: [PATCH 124/186] remove base.Entry() method calls (#362) --- .../Rewriters/VirtualEntryCallRemover.cs | 90 +++++++++++++++++++ .../Metadata/InstructionMetadata.cs | 5 +- .../StardewModdingAPI.csproj | 1 + 3 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 src/StardewModdingAPI/Framework/ModLoading/Rewriters/VirtualEntryCallRemover.cs diff --git a/src/StardewModdingAPI/Framework/ModLoading/Rewriters/VirtualEntryCallRemover.cs b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/VirtualEntryCallRemover.cs new file mode 100644 index 00000000..322a7df1 --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModLoading/Rewriters/VirtualEntryCallRemover.cs @@ -0,0 +1,90 @@ +using System; +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters +{ + /// Rewrites virtual calls to the method. + internal class VirtualEntryCallRemover : IInstructionHandler + { + /********* + ** Properties + *********/ + /// The type containing the method. + private readonly Type ToType; + + /// The name of the method. + private readonly string MethodName; + + + /********* + ** Accessors + *********/ + /// A brief noun phrase indicating what the instruction finder matches. + public string NounPhrase { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public VirtualEntryCallRemover() + { + this.ToType = typeof(Mod); + this.MethodName = nameof(Mod.Entry); + this.NounPhrase = $"{this.ToType.Name}::{this.MethodName}"; + } + + /// Perform the predefined logic for a method if applicable. + /// The assembly module containing the instruction. + /// The method definition containing the instruction. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + return InstructionHandleResult.None; + } + + /// Perform the predefined logic for an instruction if applicable. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + if (!this.IsMatch(instruction)) + return InstructionHandleResult.None; + + // get instructions comprising method call + int index = cil.Body.Instructions.IndexOf(instruction); + Instruction loadArg0 = cil.Body.Instructions[index - 2]; + Instruction loadArg1 = cil.Body.Instructions[index - 1]; + if (loadArg0.OpCode != OpCodes.Ldarg_0) + throw new InvalidOperationException($"Unexpected instruction sequence while removing virtual {this.ToType.Name}.{this.MethodName} call: found {loadArg0.OpCode.Name} instead of {OpCodes.Ldarg_0.Name}"); + if (loadArg1.OpCode != OpCodes.Ldarg_1) + throw new InvalidOperationException($"Unexpected instruction sequence while removing virtual {this.ToType.Name}.{this.MethodName} call: found {loadArg1.OpCode.Name} instead of {OpCodes.Ldarg_1.Name}"); + + // remove method call + cil.Remove(loadArg0); + cil.Remove(loadArg1); + cil.Remove(instruction); + return InstructionHandleResult.Rewritten; + } + + + /********* + ** Protected methods + *********/ + /// Get whether a CIL instruction matches. + /// The IL instruction. + protected bool IsMatch(Instruction instruction) + { + MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); + return + methodRef != null + && methodRef.DeclaringType.FullName == this.ToType.FullName + && methodRef.Name == this.MethodName; + } + } +} diff --git a/src/StardewModdingAPI/Metadata/InstructionMetadata.cs b/src/StardewModdingAPI/Metadata/InstructionMetadata.cs index 35989801..1842d5d8 100644 --- a/src/StardewModdingAPI/Metadata/InstructionMetadata.cs +++ b/src/StardewModdingAPI/Metadata/InstructionMetadata.cs @@ -90,7 +90,10 @@ namespace StardewModdingAPI.Metadata new FieldReplaceRewriter(typeof(Game1), "smoothFont", nameof(Game1.smallFont)), // SMAPI 1.9 - new TypeReferenceRewriter("StardewModdingAPI.Inheritance.ItemStackChange", typeof(ItemStackChange)) + new TypeReferenceRewriter("StardewModdingAPI.Inheritance.ItemStackChange", typeof(ItemStackChange)), + + // SMAPI 2.0 + new VirtualEntryCallRemover() // Mod.Entry changed from virtual to abstract in SMAPI 2.0, which breaks the few mods which called base.Entry() }; } } diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 8863590b..78d2bc8e 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -104,6 +104,7 @@ + From 83bc6264e44d9b385db819c15892da316955471f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 26 Sep 2017 20:46:25 -0400 Subject: [PATCH 125/186] simplify API fetch code --- .../Framework/WebApiClient.cs | 36 ++++++------------- src/StardewModdingAPI/Program.cs | 7 ++-- 2 files changed, 13 insertions(+), 30 deletions(-) diff --git a/src/StardewModdingAPI/Framework/WebApiClient.cs b/src/StardewModdingAPI/Framework/WebApiClient.cs index 8f0b403d..f3c7de28 100644 --- a/src/StardewModdingAPI/Framework/WebApiClient.cs +++ b/src/StardewModdingAPI/Framework/WebApiClient.cs @@ -1,9 +1,6 @@ using System; using System.Collections.Generic; -using System.IO; using System.Net; -using System.Text; -using System.Threading.Tasks; using Newtonsoft.Json; using StardewModdingAPI.Models; @@ -39,9 +36,9 @@ namespace StardewModdingAPI.Framework /// Get the latest SMAPI version. /// The mod keys for which to fetch the latest version. - public async Task> GetModInfoAsync(params string[] modKeys) + public IDictionary GetModInfo(params string[] modKeys) { - return await this.PostAsync>( + return this.Post>( $"v{this.Version}/mods", new ModSearchModel(modKeys) ); @@ -56,31 +53,20 @@ namespace StardewModdingAPI.Framework /// The expected response type. /// The request URL, optionally excluding the base URL. /// The body content to post. - private async Task PostAsync(string url, TBody content) + private TResult Post(string url, TBody content) { /*** ** Note: avoid HttpClient for Mac compatibility. ***/ - - // serialise content - byte[] data = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(content)); - - // build request - HttpWebRequest request = WebRequest.CreateHttp(new Uri(this.BaseUrl, url).ToString()); - request.Method = "POST"; - request.UserAgent = $"SMAPI/{this.Version}"; - request.ContentType = "application/json"; - request.ContentLength = data.Length; - using (Stream bodyStream = request.GetRequestStream()) - bodyStream.Write(data, 0, data.Length); - - // fetch data - using (WebResponse response = await request.GetResponseAsync()) - using (Stream responseStream = response.GetResponseStream()) - using (StreamReader reader = new StreamReader(responseStream)) + using (WebClient client = new WebClient()) { - string responseText = reader.ReadToEnd(); - return JsonConvert.DeserializeObject(responseText); + Uri fullUrl = new Uri(this.BaseUrl, url); + string data = JsonConvert.SerializeObject(content); + + client.Headers["Content-Type"] = "application/json"; + client.Headers["User-Agent"] = $"SMAPI/{this.Version}"; + string response = client.UploadString(fullUrl, data); + return JsonConvert.DeserializeObject(response); } } } diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index ba8c7847..e7f6f458 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -497,7 +497,7 @@ namespace StardewModdingAPI { this.Monitor.Log("Checking for SMAPI update...", LogLevel.Trace); - ModInfoModel response = client.GetModInfoAsync($"GitHub:{this.Settings.GitHubProjectName}").Result.Single().Value; + ModInfoModel response = client.GetModInfo($"GitHub:{this.Settings.GitHubProjectName}").Single().Value; if (response.Error != null) { this.Monitor.Log("Couldn't check for a new version of SMAPI. This won't affect your game, but you may not be notified of new versions if this keeps happening.", LogLevel.Warn); @@ -549,7 +549,7 @@ namespace StardewModdingAPI this.Monitor.Log($"Checking for updates to {modsByKey.Keys.Count} keys...", LogLevel.Trace); var results = ( - from entry in client.GetModInfoAsync(modsByKey.Keys.ToArray()).Result + from entry in client.GetModInfo(modsByKey.Keys.ToArray()) from mod in modsByKey[entry.Key] orderby mod.DisplayName select new { entry.Key, Mod = mod, Info = entry.Value } @@ -595,9 +595,6 @@ namespace StardewModdingAPI { this.Monitor.Log($"Couldn't check for new mod versions:\n{ex.GetLogSummary()}", LogLevel.Trace); } - - - }).Start(); } From 3c42119c8c0541a4f233e79fe591aa09c79c58cf Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 26 Sep 2017 21:08:54 -0400 Subject: [PATCH 126/186] restore AssemblyRewriters assembly for method injection This fixes a SMAPI 2.0 issue where mods would fail with MethodAccessException if they used SpriteBatch methods that got rewritten for MonoGame/XNA compatibility, because the methods SMAPI injected were internal. Moving it back into a separate assembly lets us make it public without making it visible to modders. --- README.md | 2 + .../Properties/AssemblyInfo.cs | 7 +++ .../SpriteBatchMethods.cs} | 8 ++-- ...StardewModdingAPI.AssemblyRewriters.csproj | 44 +++++++++++++++++++ .../InteractiveInstaller.cs | 4 +- src/StardewModdingAPI.sln | 13 ++++++ .../Metadata/InstructionMetadata.cs | 4 +- .../StardewModdingAPI.csproj | 7 ++- src/common.targets | 1 + src/prepare-install-package.targets | 2 + 10 files changed, 83 insertions(+), 9 deletions(-) create mode 100644 src/StardewModdingAPI.AssemblyRewriters/Properties/AssemblyInfo.cs rename src/{StardewModdingAPI/Framework/ModLoading/Rewriters/Wrappers/SpriteBatchWrapper.cs => StardewModdingAPI.AssemblyRewriters/SpriteBatchMethods.cs} (88%) create mode 100644 src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj diff --git a/README.md b/README.md index 693530d1..0f2f05a6 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,7 @@ on the wiki for the first-time setup. Mono.Cecil.dll Newtonsoft.Json.dll StardewModdingAPI + StardewModdingAPI.AssemblyRewriters.dll StardewModdingAPI.config.json StardewModdingAPI.exe StardewModdingAPI.pdb @@ -122,6 +123,7 @@ on the wiki for the first-time setup. Mods/* Mono.Cecil.dll Newtonsoft.Json.dll + StardewModdingAPI.AssemblyRewriters.dll StardewModdingAPI.config.json StardewModdingAPI.exe StardewModdingAPI.pdb diff --git a/src/StardewModdingAPI.AssemblyRewriters/Properties/AssemblyInfo.cs b/src/StardewModdingAPI.AssemblyRewriters/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..7cc6804a --- /dev/null +++ b/src/StardewModdingAPI.AssemblyRewriters/Properties/AssemblyInfo.cs @@ -0,0 +1,7 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("StardewModdingAPI.AssemblyRewriters")] +[assembly: AssemblyDescription("Contains internal SMAPI classes used during assembly rewriting that need to be public for technical reasons, but shouldn't be visible to modders.")] +[assembly: AssemblyProduct("StardewModdingAPI.AssemblyRewriters")] +[assembly: Guid("10db0676-9fc1-4771-a2c8-e2519f091e49")] diff --git a/src/StardewModdingAPI/Framework/ModLoading/Rewriters/Wrappers/SpriteBatchWrapper.cs b/src/StardewModdingAPI.AssemblyRewriters/SpriteBatchMethods.cs similarity index 88% rename from src/StardewModdingAPI/Framework/ModLoading/Rewriters/Wrappers/SpriteBatchWrapper.cs rename to src/StardewModdingAPI.AssemblyRewriters/SpriteBatchMethods.cs index 7a631f69..a7f100f2 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/Rewriters/Wrappers/SpriteBatchWrapper.cs +++ b/src/StardewModdingAPI.AssemblyRewriters/SpriteBatchMethods.cs @@ -2,16 +2,16 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; -namespace StardewModdingAPI.Framework.ModLoading.Rewriters.Wrappers +namespace StardewModdingAPI.AssemblyRewriters { - /// Wraps methods that are incompatible when converting compiled code between MonoGame and XNA. - internal class SpriteBatchWrapper : SpriteBatch + /// Provides method signatures that can be injected into mod code for compatibility between Linux/Mac or Windows. + public class SpriteBatchMethods : SpriteBatch { /********* ** Public methods *********/ /// Construct an instance. - public SpriteBatchWrapper(GraphicsDevice graphicsDevice) : base(graphicsDevice) { } + public SpriteBatchMethods(GraphicsDevice graphicsDevice) : base(graphicsDevice) { } /**** diff --git a/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj b/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj new file mode 100644 index 00000000..c8b03086 --- /dev/null +++ b/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj @@ -0,0 +1,44 @@ + + + + + Debug + x86 + {10DB0676-9FC1-4771-A2C8-E2519F091E49} + Library + Properties + StardewModdingAPI.AssemblyRewriters + StardewModdingAPI.AssemblyRewriters + v4.5 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + Properties\GlobalAssemblyInfo.cs + + + + + + + \ No newline at end of file diff --git a/src/StardewModdingAPI.Installer/InteractiveInstaller.cs b/src/StardewModdingAPI.Installer/InteractiveInstaller.cs index 83638e10..1a132e54 100644 --- a/src/StardewModdingAPI.Installer/InteractiveInstaller.cs +++ b/src/StardewModdingAPI.Installer/InteractiveInstaller.cs @@ -81,6 +81,7 @@ namespace StardewModdingApi.Installer yield return GetInstallPath("StardewModdingAPI.exe"); yield return GetInstallPath("StardewModdingAPI.config.json"); yield return GetInstallPath("StardewModdingAPI.data.json"); + yield return GetInstallPath("StardewModdingAPI.AssemblyRewriters.dll"); yield return GetInstallPath("System.ValueTuple.dll"); yield return GetInstallPath("steam_appid.txt"); @@ -97,8 +98,7 @@ namespace StardewModdingApi.Installer // obsolete yield return GetInstallPath("Mods/.cache"); // 1.3-1.4 yield return GetInstallPath("Mono.Cecil.Rocks.dll"); // 1.3–1.8 - yield return GetInstallPath("StardewModdingAPI-settings.json"); // 1.0–1.4 - yield return GetInstallPath("StardewModdingAPI.AssemblyRewriters.dll"); // 1.3–1.15.4 + yield return GetInstallPath("StardewModdingAPI-settings.json"); // 1.0-1.4 if (modsDir.Exists) { foreach (DirectoryInfo modDir in modsDir.EnumerateDirectories()) diff --git a/src/StardewModdingAPI.sln b/src/StardewModdingAPI.sln index 9d7baa51..5e8a2c93 100644 --- a/src/StardewModdingAPI.sln +++ b/src/StardewModdingAPI.sln @@ -27,6 +27,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Installer {F1A573B0-F436-472C-AE29-0B91EA6B9F8F} = {F1A573B0-F436-472C-AE29-0B91EA6B9F8F} EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.AssemblyRewriters", "StardewModdingAPI.AssemblyRewriters\StardewModdingAPI.AssemblyRewriters.csproj", "{10DB0676-9FC1-4771-A2C8-E2519F091E49}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Tests", "StardewModdingAPI.Tests\StardewModdingAPI.Tests.csproj", "{36CCB19E-92EB-48C7-9615-98EEFD45109B}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StardewModdingAPI.Web", "StardewModdingAPI.Web\StardewModdingAPI.Web.csproj", "{A308F679-51A3-4006-92D5-BAEC7EBD01A1}" @@ -79,6 +81,16 @@ Global {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|Mixed Platforms.Build.0 = Release|x86 {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|x86.ActiveCfg = Release|x86 {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|x86.Build.0 = Release|x86 + {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Debug|Any CPU.ActiveCfg = Debug|x86 + {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 + {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Debug|Mixed Platforms.Build.0 = Debug|x86 + {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Debug|x86.ActiveCfg = Debug|x86 + {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Debug|x86.Build.0 = Debug|x86 + {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Release|Any CPU.ActiveCfg = Release|x86 + {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Release|Mixed Platforms.ActiveCfg = Release|x86 + {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Release|Mixed Platforms.Build.0 = Release|x86 + {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Release|x86.ActiveCfg = Release|x86 + {10DB0676-9FC1-4771-A2C8-E2519F091E49}.Release|x86.Build.0 = Release|x86 {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|Any CPU.ActiveCfg = Debug|x86 {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|Mixed Platforms.Build.0 = Debug|x86 @@ -106,6 +118,7 @@ Global HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution + {10DB0676-9FC1-4771-A2C8-E2519F091E49} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} {36CCB19E-92EB-48C7-9615-98EEFD45109B} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} {2AA02FB6-FF03-41CF-A215-2EE60AB4F5DC} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} EndGlobalSection diff --git a/src/StardewModdingAPI/Metadata/InstructionMetadata.cs b/src/StardewModdingAPI/Metadata/InstructionMetadata.cs index 1842d5d8..e8e5411c 100644 --- a/src/StardewModdingAPI/Metadata/InstructionMetadata.cs +++ b/src/StardewModdingAPI/Metadata/InstructionMetadata.cs @@ -1,10 +1,10 @@ using System.Collections.Generic; using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI.AssemblyRewriters; using StardewModdingAPI.Events; using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Framework.ModLoading.Finders; using StardewModdingAPI.Framework.ModLoading.Rewriters; -using StardewModdingAPI.Framework.ModLoading.Rewriters.Wrappers; using StardewValley; namespace StardewModdingAPI.Metadata @@ -79,7 +79,7 @@ namespace StardewModdingAPI.Metadata ** rewrite CIL to fix incompatible code ****/ // crossplatform - new MethodParentRewriter(typeof(SpriteBatch), typeof(SpriteBatchWrapper), onlyIfPlatformChanged: true), + new MethodParentRewriter(typeof(SpriteBatch), typeof(SpriteBatchMethods), onlyIfPlatformChanged: true), // Stardew Valley 1.2 new FieldToPropertyRewriter(typeof(Game1), nameof(Game1.activeClickableMenu)), diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 78d2bc8e..3721a11b 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -107,7 +107,6 @@ - @@ -267,6 +266,12 @@ + + + {10db0676-9fc1-4771-a2c8-e2519f091e49} + StardewModdingAPI.AssemblyRewriters + + \ No newline at end of file diff --git a/src/common.targets b/src/common.targets index c450b3a5..ee138524 100644 --- a/src/common.targets +++ b/src/common.targets @@ -83,6 +83,7 @@ + diff --git a/src/prepare-install-package.targets b/src/prepare-install-package.targets index 44997fef..f2a2b23c 100644 --- a/src/prepare-install-package.targets +++ b/src/prepare-install-package.targets @@ -27,6 +27,7 @@ + @@ -40,6 +41,7 @@ + From 5f7640100b60d0c31a9193906eb5f947bf113dbb Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 27 Sep 2017 20:47:01 -0400 Subject: [PATCH 127/186] update for 2.0 beta release --- src/GlobalAssemblyInfo.cs | 4 ++-- src/StardewModdingAPI/Constants.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/GlobalAssemblyInfo.cs b/src/GlobalAssemblyInfo.cs index 882e3bda..196d67c5 100644 --- a/src/GlobalAssemblyInfo.cs +++ b/src/GlobalAssemblyInfo.cs @@ -2,5 +2,5 @@ using System.Reflection; using System.Runtime.InteropServices; [assembly: ComVisible(false)] -[assembly: AssemblyVersion("1.15.4.0")] -[assembly: AssemblyFileVersion("1.15.4.0")] +[assembly: AssemblyVersion("2.0.0.0")] +[assembly: AssemblyFileVersion("2.0.0.0")] diff --git a/src/StardewModdingAPI/Constants.cs b/src/StardewModdingAPI/Constants.cs index 5b1d7737..4d0a9ca9 100644 --- a/src/StardewModdingAPI/Constants.cs +++ b/src/StardewModdingAPI/Constants.cs @@ -29,7 +29,7 @@ namespace StardewModdingAPI ** Public ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } = new SemanticVersion(2, 0, 0, $"alpha-{DateTime.UtcNow:yyyyMMddHHmm}"); + public static ISemanticVersion ApiVersion { get; } = new SemanticVersion(2, 0, 0, "beta.1"); /// The minimum supported version of Stardew Valley. public static ISemanticVersion MinimumGameVersion { get; } = new SemanticVersion("1.2.30"); From 29232ffd459f67813fd599cc074610519e3a5148 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 1 Oct 2017 11:23:50 -0400 Subject: [PATCH 128/186] update mod key --- src/StardewModdingAPI/StardewModdingAPI.config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StardewModdingAPI/StardewModdingAPI.config.json b/src/StardewModdingAPI/StardewModdingAPI.config.json index 662254e3..8f5e6e47 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.config.json +++ b/src/StardewModdingAPI/StardewModdingAPI.config.json @@ -718,7 +718,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, { // Farm Expansion - "ID": "3888bdfd-73f6-4776-8bb7-8ad45aea1915 | AdvizeFarmExpansionMod-2-0 | AdvizeFarmExpansionMod-2-0-5", // changed in 2.0 and 2.0.5 + "ID": "3888bdfd-73f6-4776-8bb7-8ad45aea1915 | AdvizeFarmExpansionMod-2-0 | AdvizeFarmExpansionMod-2-0-5 | Advize.FarmExpansion", // changed in 2.0, 2.0.5, and 3.0 "UpdateKeys": [ "Nexus:130" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { From bd4ed4382909ff368536a7ce8f0e5f514658d288 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 1 Oct 2017 14:08:28 -0400 Subject: [PATCH 129/186] fix errors caused by content managers finalizing asynchronously --- .../Framework/SContentManager.cs | 159 ++++++++++-------- 1 file changed, 87 insertions(+), 72 deletions(-) diff --git a/src/StardewModdingAPI/Framework/SContentManager.cs b/src/StardewModdingAPI/Framework/SContentManager.cs index 33403841..43de6e96 100644 --- a/src/StardewModdingAPI/Framework/SContentManager.cs +++ b/src/StardewModdingAPI/Framework/SContentManager.cs @@ -128,8 +128,11 @@ namespace StardewModdingAPI.Framework /// The asset path relative to the loader root directory, not including the .xnb extension. public bool IsLoaded(string assetName) { - assetName = this.NormaliseAssetName(assetName); - return this.IsNormalisedKeyLoaded(assetName); + lock (this.Cache) + { + assetName = this.NormaliseAssetName(assetName); + return this.IsNormalisedKeyLoaded(assetName); + } } /// Load an asset that has been processed by the content pipeline. @@ -146,38 +149,41 @@ namespace StardewModdingAPI.Framework /// The content manager instance for which to load the asset. public T LoadFor(string assetName, ContentManager instance) { - assetName = this.NormaliseAssetName(assetName); + lock (this.Cache) + { + assetName = this.NormaliseAssetName(assetName); - // skip if already loaded - if (this.IsNormalisedKeyLoaded(assetName)) - { - this.TrackAssetLoader(assetName, instance); - return base.Load(assetName); - } - - // load asset - T data; - if (this.AssetsBeingLoaded.Contains(assetName)) - { - this.Monitor.Log($"Broke loop while loading asset '{assetName}'.", LogLevel.Warn); - this.Monitor.Log($"Bypassing mod loaders for this asset. Stack trace:\n{Environment.StackTrace}", LogLevel.Trace); - data = base.Load(assetName); - } - else - { - data = this.AssetsBeingLoaded.Track(assetName, () => + // skip if already loaded + if (this.IsNormalisedKeyLoaded(assetName)) { - IAssetInfo info = new AssetInfo(this.GetLocale(), assetName, typeof(T), this.NormaliseAssetName); - IAssetData asset = this.ApplyLoader(info) ?? new AssetDataForObject(info, base.Load(assetName), this.NormaliseAssetName); - asset = this.ApplyEditors(info, asset); - return (T)asset.Data; - }); - } + this.TrackAssetLoader(assetName, instance); + return base.Load(assetName); + } - // update cache & return data - this.Cache[assetName] = data; - this.TrackAssetLoader(assetName, instance); - return data; + // load asset + T data; + if (this.AssetsBeingLoaded.Contains(assetName)) + { + this.Monitor.Log($"Broke loop while loading asset '{assetName}'.", LogLevel.Warn); + this.Monitor.Log($"Bypassing mod loaders for this asset. Stack trace:\n{Environment.StackTrace}", LogLevel.Trace); + data = base.Load(assetName); + } + else + { + data = this.AssetsBeingLoaded.Track(assetName, () => + { + IAssetInfo info = new AssetInfo(this.GetLocale(), assetName, typeof(T), this.NormaliseAssetName); + IAssetData asset = this.ApplyLoader(info) ?? new AssetDataForObject(info, base.Load(assetName), this.NormaliseAssetName); + asset = this.ApplyEditors(info, asset); + return (T)asset.Data; + }); + } + + // update cache & return data + this.Cache[assetName] = data; + this.TrackAssetLoader(assetName, instance); + return data; + } } /// Inject an asset into the cache. @@ -186,9 +192,12 @@ namespace StardewModdingAPI.Framework /// The asset value. public void Inject(string assetName, T value) { - assetName = this.NormaliseAssetName(assetName); - this.Cache[assetName] = value; - this.TrackAssetLoader(assetName, this); + lock (this.Cache) + { + assetName = this.NormaliseAssetName(assetName); + this.Cache[assetName] = value; + this.TrackAssetLoader(assetName, this); + } } /// Get the current content locale. @@ -200,16 +209,19 @@ namespace StardewModdingAPI.Framework /// Get the cached asset keys. public IEnumerable GetAssetKeys() { - IEnumerable GetAllAssetKeys() + lock (this.Cache) { - foreach (string cacheKey in this.Cache.Keys) + IEnumerable GetAllAssetKeys() { - this.ParseCacheKey(cacheKey, out string assetKey, out string _); - yield return assetKey; + foreach (string cacheKey in this.Cache.Keys) + { + this.ParseCacheKey(cacheKey, out string assetKey, out string _); + yield return assetKey; + } } - } - return GetAllAssetKeys().Distinct(); + return GetAllAssetKeys().Distinct(); + } } /// Purge assets from the cache that match one of the interceptors. @@ -248,45 +260,48 @@ namespace StardewModdingAPI.Framework /// Returns whether any cache entries were invalidated. public bool InvalidateCache(Func predicate, bool dispose = false) { - // find matching asset keys - HashSet purgeCacheKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase); - HashSet purgeAssetKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase); - foreach (string cacheKey in this.Cache.Keys) + lock (this.Cache) { - this.ParseCacheKey(cacheKey, out string assetKey, out _); - Type type = this.Cache[cacheKey].GetType(); - if (predicate(assetKey, type)) + // find matching asset keys + HashSet purgeCacheKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase); + HashSet purgeAssetKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase); + foreach (string cacheKey in this.Cache.Keys) { - purgeAssetKeys.Add(assetKey); - purgeCacheKeys.Add(cacheKey); + this.ParseCacheKey(cacheKey, out string assetKey, out _); + Type type = this.Cache[cacheKey].GetType(); + if (predicate(assetKey, type)) + { + purgeAssetKeys.Add(assetKey); + purgeCacheKeys.Add(cacheKey); + } } - } - // purge assets - foreach (string key in purgeCacheKeys) - { - if (dispose && this.Cache[key] is IDisposable disposable) - disposable.Dispose(); - this.Cache.Remove(key); - this.AssetLoaders.Remove(key); - } + // purge assets + foreach (string key in purgeCacheKeys) + { + if (dispose && this.Cache[key] is IDisposable disposable) + disposable.Dispose(); + this.Cache.Remove(key); + this.AssetLoaders.Remove(key); + } - // reload core game assets - int reloaded = 0; - foreach (string key in purgeAssetKeys) - { - if (this.CoreAssets.ReloadForKey(this, key)) - reloaded++; - } + // reload core game assets + int reloaded = 0; + foreach (string key in purgeAssetKeys) + { + if (this.CoreAssets.ReloadForKey(this, key)) + reloaded++; + } - // report result - if (purgeCacheKeys.Any()) - { - this.Monitor.Log($"Invalidated {purgeCacheKeys.Count} cache entries for {purgeAssetKeys.Count} asset keys: {string.Join(", ", purgeCacheKeys.OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase))}. Reloaded {reloaded} core assets.", LogLevel.Trace); - return true; + // report result + if (purgeCacheKeys.Any()) + { + this.Monitor.Log($"Invalidated {purgeCacheKeys.Count} cache entries for {purgeAssetKeys.Count} asset keys: {string.Join(", ", purgeCacheKeys.OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase))}. Reloaded {reloaded} core assets.", LogLevel.Trace); + return true; + } + this.Monitor.Log("Invalidated 0 cache entries.", LogLevel.Trace); + return false; } - this.Monitor.Log("Invalidated 0 cache entries.", LogLevel.Trace); - return false; } /// Dispose assets for the given content manager shim. From 365da8e6e4aa03b66bf07cd8377a6062beed08f0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 2 Oct 2017 21:39:51 -0400 Subject: [PATCH 130/186] detect use of 'dynamic' in mod code --- release-notes.md | 2 +- .../Framework/ModLoading/AssemblyLoader.cs | 18 +++++++++++++++++- .../ModLoading/InstructionHandleResult.cs | 5 ++++- .../Metadata/InstructionMetadata.cs | 1 + src/StardewModdingAPI/Program.cs | 2 +- 5 files changed, 24 insertions(+), 4 deletions(-) diff --git a/release-notes.md b/release-notes.md index 4f4667d9..a1ee0f6e 100644 --- a/release-notes.md +++ b/release-notes.md @@ -4,7 +4,7 @@ For players: * SMAPI now alerts you when mods have new versions available. -* SMAPI now warns you about mods which may impact game stability. +* SMAPI now warns you about mods which may impact game stability or compatibility. * The console is now simpler and easier to read, and adjusts its colors to fit your terminal background color. * Renamed installer folder to avoid confusion. * Updated compatibility list. diff --git a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs index 32988f97..1e3c4a05 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs @@ -25,6 +25,9 @@ namespace StardewModdingAPI.Framework.ModLoading /// Encapsulates monitoring and logging. private readonly IMonitor Monitor; + /// Whether to enable developer mode logging. + private readonly bool IsDeveloperMode; + /********* ** Public methods @@ -32,9 +35,11 @@ namespace StardewModdingAPI.Framework.ModLoading /// Construct an instance. /// The current game platform. /// Encapsulates monitoring and logging. - public AssemblyLoader(Platform targetPlatform, IMonitor monitor) + /// Whether to enable developer mode logging. + public AssemblyLoader(Platform targetPlatform, IMonitor monitor, bool isDeveloperMode) { this.Monitor = monitor; + this.IsDeveloperMode = isDeveloperMode; this.AssemblyMap = Constants.GetAssemblyMap(targetPlatform); // generate type => assembly lookup for types which should be rewritten @@ -276,6 +281,17 @@ namespace StardewModdingAPI.Framework.ModLoading this.Monitor.LogOnce(loggedMessages, $"{mod.DisplayName} seems to change the save serialiser. It may change your saves in such a way that they won't work without this mod in the future.", LogLevel.Warn); break; + case InstructionHandleResult.DetectedDynamic: + this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected 'dynamic' keyword ({handler.NounPhrase}) in assembly {filename}."); + this.Monitor.LogOnce(loggedMessages, $"{mod.DisplayName} uses the 'dynamic' keyword, which isn't compatible with Stardew Valley on Linux or Mac.", +#if SMAPI_FOR_WINDOWS + this.IsDeveloperMode ? LogLevel.Warn : LogLevel.Debug +#else + LogLevel.Warn +#endif + ); + break; + case InstructionHandleResult.None: break; diff --git a/src/StardewModdingAPI/Framework/ModLoading/InstructionHandleResult.cs b/src/StardewModdingAPI/Framework/ModLoading/InstructionHandleResult.cs index 95d57ef2..0ae598fc 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/InstructionHandleResult.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/InstructionHandleResult.cs @@ -16,6 +16,9 @@ namespace StardewModdingAPI.Framework.ModLoading DetectedGamePatch, /// The instruction is compatible, but affects the save serializer in a way that may make saves unloadable without the mod. - DetectedSaveSerialiser + DetectedSaveSerialiser, + + /// The instruction is compatible, but uses the dynamic keyword which won't work on Linux/Mac. + DetectedDynamic } } diff --git a/src/StardewModdingAPI/Metadata/InstructionMetadata.cs b/src/StardewModdingAPI/Metadata/InstructionMetadata.cs index e8e5411c..3346f1ac 100644 --- a/src/StardewModdingAPI/Metadata/InstructionMetadata.cs +++ b/src/StardewModdingAPI/Metadata/InstructionMetadata.cs @@ -71,6 +71,7 @@ namespace StardewModdingAPI.Metadata ** detect code which may impact game stability ****/ new TypeFinder("Harmony.HarmonyInstance", InstructionHandleResult.DetectedGamePatch), + new TypeFinder("System.Runtime.CompilerServices.CallSite", InstructionHandleResult.DetectedDynamic), new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.serializer), InstructionHandleResult.DetectedSaveSerialiser), new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.farmerSerializer), InstructionHandleResult.DetectedSaveSerialiser), new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.locationSerializer), InstructionHandleResult.DetectedSaveSerialiser), diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index e7f6f458..304de15d 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -626,7 +626,7 @@ namespace StardewModdingAPI { void TrackSkip(IModMetadata mod, string reasonPhrase) => skippedMods[mod] = reasonPhrase; - AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.TargetPlatform, this.Monitor); + AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.TargetPlatform, this.Monitor, this.Settings.DeveloperMode); AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => modAssemblyLoader.ResolveAssembly(e.Name); foreach (IModMetadata metadata in mods) { From 361051b43a1ea27ef99de013c5ec7c207fb65141 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 2 Oct 2017 21:41:09 -0400 Subject: [PATCH 131/186] + release note --- release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release-notes.md b/release-notes.md index a1ee0f6e..5b102df3 100644 --- a/release-notes.md +++ b/release-notes.md @@ -9,6 +9,7 @@ For players: * Renamed installer folder to avoid confusion. * Updated compatibility list. * Fixed update check errors on Linux/Mac. +* Fixed collection-changed errors during startup for some players. For mod developers: * Added support for editing, injecting, and reloading XNB data loaded by the game at any time. From 627f20b9c3d54e572262316e73f12050fd0dba54 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 2 Oct 2017 21:43:15 -0400 Subject: [PATCH 132/186] update one mod ID --- src/StardewModdingAPI/StardewModdingAPI.config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StardewModdingAPI/StardewModdingAPI.config.json b/src/StardewModdingAPI/StardewModdingAPI.config.json index 8f5e6e47..9227464d 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.config.json +++ b/src/StardewModdingAPI/StardewModdingAPI.config.json @@ -1616,7 +1616,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha }, { // Slower Fence Decay - "ID": "SPDSlowFenceDecay", + "ID": "SPDSlowFenceDecay | Speeder.SlowerFenceDecay", // changed in 0.5.2-pathoschild-update "UpdateKeys": [ "Nexus:252" ], "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { From 6d7449d00b2aaf2909caf288aeb24a8c2fb94477 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 4 Oct 2017 23:36:03 -0400 Subject: [PATCH 133/186] fix error when checking for updates to a mod with no data record --- src/StardewModdingAPI/Program.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 304de15d..7dfdc745 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -571,8 +571,13 @@ namespace StardewModdingAPI } // track update - ISemanticVersion localVersion = new SemanticVersion(mod.DataRecord?.GetLocalVersionForUpdateChecks(mod.Manifest.Version.ToString())); - ISemanticVersion latestVersion = new SemanticVersion(mod.DataRecord?.GetRemoteVersionForUpdateChecks(new SemanticVersion(info.Version).ToString())); + ISemanticVersion localVersion = mod.DataRecord != null + ? new SemanticVersion(mod.DataRecord.GetLocalVersionForUpdateChecks(mod.Manifest.Version.ToString())) + : mod.Manifest.Version; + ISemanticVersion latestVersion = new SemanticVersion(mod.DataRecord != null + ? mod.DataRecord.GetRemoteVersionForUpdateChecks(new SemanticVersion(info.Version).ToString()) + : info.Version + ); bool isUpdate = latestVersion.IsNewerThan(localVersion); this.VerboseLog($" {mod.DisplayName} ({result.Key}): {(isUpdate ? $"{mod.Manifest.Version}{(!localVersion.Equals(mod.Manifest.Version) ? $" [{localVersion}]" : "")} => {info.Version}{(!latestVersion.Equals(new SemanticVersion(info.Version)) ? $" [{latestVersion}]" : "")}" : "OK")}."); if (isUpdate) From 6f3fc68dafcaaaab59352378a9cdae41040e271a Mon Sep 17 00:00:00 2001 From: Death Date: Sat, 7 Oct 2017 00:58:17 -0500 Subject: [PATCH 134/186] Updates exported mod zip files to use a base folder Instead of all of the mod files being in the root directory of the zip file, they are now in /ModName/, making it easier for modders to export upload-ready zip files of their mods. --- build/smapi.targets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/smapi.targets b/build/smapi.targets index 58737fba..ab7fb71f 100644 --- a/build/smapi.targets +++ b/build/smapi.targets @@ -36,7 +36,7 @@ { // get file info string filePath = file.ItemSpec; - string entryName = file.GetMetadata("RecursiveDir") + file.GetMetadata("Filename") + file.GetMetadata("Extension"); + string entryName = ModName + '/' + file.GetMetadata("RecursiveDir") + file.GetMetadata("Filename") + file.GetMetadata("Extension"); if (new FileInfo(filePath).Directory.Name.Equals("i18n", StringComparison.InvariantCultureIgnoreCase)) entryName = Path.Combine("i18n", entryName); From 78e59e1a48e6d9d2583c9adf54422d15e6e7074e Mon Sep 17 00:00:00 2001 From: Death Date: Sat, 7 Oct 2017 17:54:11 -0500 Subject: [PATCH 135/186] add version support to zip filenames (#7) --- build/smapi.targets | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/build/smapi.targets b/build/smapi.targets index ab7fb71f..88408994 100644 --- a/build/smapi.targets +++ b/build/smapi.targets @@ -14,8 +14,10 @@ + + data = (IDictionary)new JavaScriptSerializer().DeserializeObject(json); + IDictionary version = (IDictionary)data["Version"]; + + // Store our version numbers for ease of use + int major = (int)version["MajorVersion"]; + int minor = (int)version["MinorVersion"]; + int patch = (int)version["PatchVersion"]; + + string fileName = String.Format("{0}-{1}.{2}.{3}.zip", ModName, major, minor, patch); + // clear old zip file if present - string zipPath = Path.Combine(OutputFolderPath, ModName + ".zip"); + string zipPath = Path.Combine(OutputFolderPath, fileName); if (File.Exists(zipPath)) File.Delete(zipPath); From 4d32b37790442b8f19379ac89e72f0ecb161b549 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 7 Oct 2017 20:16:04 -0400 Subject: [PATCH 136/186] switch create-zip task to class type to simplify encapsulation --- build/smapi.targets | 134 ++++++++++++++++++++++++++------------------ 1 file changed, 81 insertions(+), 53 deletions(-) diff --git a/build/smapi.targets b/build/smapi.targets index 88408994..3512d042 100644 --- a/build/smapi.targets +++ b/build/smapi.targets @@ -1,6 +1,6 @@ @@ -470,33 +469,3 @@ For SMAPI developers: * 0.3 (2016-03-01, [log](https://github.com/Pathoschild/SMAPI/compare/Alpha0.2...0.3)) * 0.2 (2016-02-29, [log](https://github.com/Pathoschild/SMAPI/compare/Alpha0.1...Alpha0.2) * 0.1 (2016-02-28) -======= -## Release notes -### 1.6 -* Added support for deploying mod files into `Mods` automatically. -* Added a build error if a game folder is found, but doesn't contain Stardew Valley or SMAPI. - -### 1.5 -* Added support for setting a custom game path globally. -* Added default GOG path on Mac. - -### 1.4 -* Fixed detection of non-default game paths on 32-bit Windows. -* Removed support for SilVerPLuM (discontinued). -* Removed support for overriding the target platform (no longer needed since SMAPI crossplatforms mods automatically). - -### 1.3 -* Added support for non-default game paths on Windows. - -### 1.2 -* Exclude game binaries from mod build output. - -### 1.1 -* Added support for overriding the target platform. - -### 1.0 -* Initial release. -* Added support for detecting the game path automatically. -* Added support for injecting XNA/MonoGame references automatically based on the OS. -* Added support for mod builders like SilVerPLuM. ->>>>>>> mod-build-config/develop diff --git a/src/StardewModdingAPI.ModBuildConfig/README.md b/src/StardewModdingAPI.ModBuildConfig/README.md new file mode 100644 index 00000000..c261e705 --- /dev/null +++ b/src/StardewModdingAPI.ModBuildConfig/README.md @@ -0,0 +1,121 @@ +**Stardew.ModBuildConfig** is an open-source NuGet package which automates the build configuration +for [Stardew Valley](http://stardewvalley.net/) [SMAPI](https://github.com/Pathoschild/SMAPI) mods. + +The package... + +* lets you write your mod once, and compile it on any computer. It detects the current platform + (Linux, Mac, or Windows) and game install path, and injects the right references automatically. +* configures Visual Studio so you can debug into the mod code when the game is running (_Windows + only_). +* packages the mod automatically into the game's mod folder when you build the code (_optional_). + +## Contents +* [Install](#install) +* [Simplify mod development](#simplify-mod-development) +* [Troubleshoot](#troubleshoot) +* [Versions](#versions) + +## Install +**When creating a new mod:** + +1. Create an empty library project. +2. Reference the [`Pathoschild.Stardew.ModBuildConfig` NuGet package](https://www.nuget.org/packages/Pathoschild.Stardew.ModBuildConfig). +3. [Write your code](http://canimod.com/guides/creating-a-smapi-mod). +4. Compile on any platform. + +**When migrating an existing mod:** + +1. Remove any project references to `Microsoft.Xna.*`, `MonoGame`, Stardew Valley, + `StardewModdingAPI`, and `xTile`. +2. Reference the [`Pathoschild.Stardew.ModBuildConfig` NuGet package](https://www.nuget.org/packages/Pathoschild.Stardew.ModBuildConfig). +3. Compile on any platform. + +## Simplify mod development +### Package your mod into the game folder automatically +You can copy your mod files into the `Mods` folder automatically each time you build, so you don't +need to do it manually: + +1. Edit your mod's `.csproj` file. +2. Add this block above the first `` line: + + ```xml + $(MSBuildProjectName) + ``` + +That's it! Each time you build, the files in `\Mods\` will be updated with +your `manifest.json`, build output, and any `i18n` files. + +Notes: +* To add custom files, just [add them to the build output](https://stackoverflow.com/a/10828462/262123). +* To customise the folder name, just replace `$(MSBuildProjectName)` with the folder name you want. +* If your project references another mod, make sure the reference is [_not_ marked 'copy local'](https://msdn.microsoft.com/en-us/library/t1zz5y8c(v=vs.100).aspx). + +### Debug into the mod code (Windows-only) +Stepping into your mod code when the game is running is straightforward, since this package injects +the configuration automatically. To do it: + +1. [Package your mod into the game folder automatically](#package-your-mod-into-the-game-folder-automatically). +2. Launch the project with debugging in Visual Studio or MonoDevelop. + +This will deploy your mod files into the game folder, launch SMAPI, and attach a debugger +automatically. Now you can step through your code, set breakpoints, etc. + +### Create release zips automatically (Windows-only) +You can create the mod package automatically when you build: + +1. Edit your mod's `.csproj` file. +2. Add this block above the first `` line: + + ```xml + $(SolutionDir)\_releases + ``` + +That's it! Each time you build, the mod files will be zipped into `_releases\.zip`. (You +can change the value to save the zips somewhere else.) + +## Troubleshoot +### "Failed to find the game install path" +That error means the package couldn't figure out where the game is installed. You need to specify +the game location yourself. There's two ways to do that: + +* **Option 1: set the path globally.** + _This will apply to every project that uses version 1.5+ of package._ + + 1. Get the full folder path containing the Stardew Valley executable. + 2. Create this file path: + + platform | path + --------- | ---- + Linux/Mac | `~/stardewvalley.targets` + Windows | `%USERPROFILE%\stardewvalley.targets` + + 3. Save the file with this content: + + ```xml + + + PATH_HERE + + + ``` + + 4. Replace `PATH_HERE` with your custom game install path. + +* **Option 2: set the path in the project file.** + _(You'll need to do it for every project that uses the package.)_ + 1. Get the folder path containing the Stardew Valley `.exe` file. + 2. Add this to your `.csproj` file under the ` + PATH_HERE + + ``` + + 3. Replace `PATH_HERE` with your custom game install path. + +The configuration will check your custom path first, then fall back to the default paths (so it'll +still compile on a different computer). + +## Versions +See [release notes](release-notes.md). diff --git a/assets/nuget-icon.pdn b/src/StardewModdingAPI.ModBuildConfig/assets/nuget-icon.pdn similarity index 100% rename from assets/nuget-icon.pdn rename to src/StardewModdingAPI.ModBuildConfig/assets/nuget-icon.pdn diff --git a/assets/nuget-icon.png b/src/StardewModdingAPI.ModBuildConfig/assets/nuget-icon.png similarity index 100% rename from assets/nuget-icon.png rename to src/StardewModdingAPI.ModBuildConfig/assets/nuget-icon.png diff --git a/build/smapi.targets b/src/StardewModdingAPI.ModBuildConfig/build/smapi.targets similarity index 100% rename from build/smapi.targets rename to src/StardewModdingAPI.ModBuildConfig/build/smapi.targets diff --git a/package.nuspec b/src/StardewModdingAPI.ModBuildConfig/package.nuspec similarity index 100% rename from package.nuspec rename to src/StardewModdingAPI.ModBuildConfig/package.nuspec diff --git a/src/StardewModdingAPI.ModBuildConfig/release-notes.md b/src/StardewModdingAPI.ModBuildConfig/release-notes.md new file mode 100644 index 00000000..ff2734f8 --- /dev/null +++ b/src/StardewModdingAPI.ModBuildConfig/release-notes.md @@ -0,0 +1,28 @@ +## Release notes +### 1.6 +* Added support for deploying mod files into `Mods` automatically. +* Added a build error if a game folder is found, but doesn't contain Stardew Valley or SMAPI. + +### 1.5 +* Added support for setting a custom game path globally. +* Added default GOG path on Mac. + +### 1.4 +* Fixed detection of non-default game paths on 32-bit Windows. +* Removed support for SilVerPLuM (discontinued). +* Removed support for overriding the target platform (no longer needed since SMAPI crossplatforms mods automatically). + +### 1.3 +* Added support for non-default game paths on Windows. + +### 1.2 +* Exclude game binaries from mod build output. + +### 1.1 +* Added support for overriding the target platform. + +### 1.0 +* Initial release. +* Added support for detecting the game path automatically. +* Added support for injecting XNA/MonoGame references automatically based on the OS. +* Added support for mod builders like SilVerPLuM. From 929dccb75a1405737975d76648e015a3e7c00177 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 7 Oct 2017 23:07:10 -0400 Subject: [PATCH 140/186] reorganise repo structure --- src/.editorconfig => .editorconfig | 0 {src => build}/GlobalAssemblyInfo.cs | 0 {src => build}/common.targets | 0 .../prepare-install-package.targets | 0 CONTRIBUTING.md => docs/CONTRIBUTING.md | 0 LICENSE => docs/LICENSE.md | 0 README.md => docs/README.md | 0 release-notes.md => docs/release-notes.md | 0 .../README.md | 0 .../assets/nuget-icon.pdn | Bin .../assets/nuget-icon.png | Bin .../build/smapi.targets | 0 .../package.nuspec | 0 .../release-notes.md | 0 .../Properties/AssemblyInfo.cs | 0 .../SpriteBatchMethods.cs | 0 ...StardewModdingAPI.AssemblyRewriters.csproj | 6 +-- .../Enums/Platform.cs | 0 .../Enums/ScriptAction.cs | 0 .../InteractiveInstaller.cs | 0 .../Program.cs | 0 .../Properties/AssemblyInfo.cs | 0 .../StardewModdingAPI.Installer.csproj | 8 ++-- .../readme.txt | 0 .../ModInfoModel.cs | 0 .../ModSeachModel.cs | 0 .../StardewModdingAPI.Models.projitems | 0 .../StardewModdingAPI.Models.shproj | 0 .../Core/ModResolverTests.cs | 0 .../Core/TranslationTests.cs | 0 .../Properties/AssemblyInfo.cs | 0 .../Sample.cs | 0 .../StardewModdingAPI.Tests.csproj | 8 ++-- .../Utilities/SDateTests.cs | 0 .../Utilities/SemanticVersionTests.cs | 0 .../packages.config | 0 .../Controllers/ModsController.cs | 0 .../ConfigModels/ModUpdateCheckConfig.cs | 0 .../InternalControllerFeatureProvider.cs | 0 .../ModRepositories/BaseRepository.cs | 0 .../ModRepositories/ChucklefishRepository.cs | 0 .../ModRepositories/GitHubRepository.cs | 0 .../ModRepositories/IModRepository.cs | 0 .../ModRepositories/NexusRepository.cs | 0 .../Framework/RewriteSubdomainRule.cs | 0 .../Framework/VersionConstraint.cs | 0 .../Program.cs | 0 .../Properties/AssemblyInfo.cs | 0 .../Properties/launchSettings.json | 0 .../StardewModdingAPI.Web.csproj | 6 +-- .../Startup.cs | 0 .../appsettings.Development.json | 0 .../appsettings.json | 0 src/{StardewModdingAPI.sln => SMAPI.sln} | 44 +++++++++++------- ....sln.DotSettings => SMAPI.sln.DotSettings} | 0 src/{StardewModdingAPI => SMAPI}/App.config | 0 src/{StardewModdingAPI => SMAPI}/Constants.cs | 0 .../ContentSource.cs | 0 src/{StardewModdingAPI => SMAPI}/Context.cs | 0 .../Events/ChangeType.cs | 0 .../Events/ContentEvents.cs | 0 .../Events/ControlEvents.cs | 0 .../Events/EventArgsClickableMenuChanged.cs | 0 .../Events/EventArgsClickableMenuClosed.cs | 0 .../EventArgsControllerButtonPressed.cs | 0 .../EventArgsControllerButtonReleased.cs | 0 .../EventArgsControllerTriggerPressed.cs | 0 .../EventArgsControllerTriggerReleased.cs | 0 .../Events/EventArgsCurrentLocationChanged.cs | 0 .../Events/EventArgsGameLocationsChanged.cs | 0 .../Events/EventArgsInput.cs | 0 .../Events/EventArgsIntChanged.cs | 0 .../Events/EventArgsInventoryChanged.cs | 0 .../Events/EventArgsKeyPressed.cs | 0 .../Events/EventArgsKeyboardStateChanged.cs | 0 .../Events/EventArgsLevelUp.cs | 0 .../Events/EventArgsLocationObjectsChanged.cs | 0 .../Events/EventArgsMineLevelChanged.cs | 0 .../Events/EventArgsMouseStateChanged.cs | 0 .../Events/EventArgsValueChanged.cs | 0 .../Events/GameEvents.cs | 0 .../Events/GraphicsEvents.cs | 0 .../Events/InputEvents.cs | 0 .../Events/ItemStackChange.cs | 0 .../Events/LocationEvents.cs | 0 .../Events/MenuEvents.cs | 0 .../Events/MineEvents.cs | 0 .../Events/PlayerEvents.cs | 0 .../Events/SaveEvents.cs | 0 .../Events/TimeEvents.cs | 0 .../Framework/Command.cs | 0 .../Framework/CommandManager.cs | 0 .../Framework/Content/AssetData.cs | 0 .../Content/AssetDataForDictionary.cs | 0 .../Framework/Content/AssetDataForImage.cs | 0 .../Framework/Content/AssetDataForObject.cs | 0 .../Framework/Content/AssetInfo.cs | 0 .../Framework/ContentManagerShim.cs | 0 .../Framework/CursorPosition.cs | 0 .../Framework/DeprecationLevel.cs | 0 .../Framework/DeprecationManager.cs | 0 .../SAssemblyLoadFailedException.cs | 0 .../Exceptions/SContentLoadException.cs | 0 .../Framework/Exceptions/SParseException.cs | 0 .../Framework/GameVersion.cs | 0 .../Framework/IModMetadata.cs | 0 .../Framework/InternalExtensions.cs | 0 .../Logging/ConsoleInterceptionManager.cs | 0 .../Logging/InterceptingTextWriter.cs | 0 .../Framework/Logging/LogFileManager.cs | 0 .../Framework/ModHelpers/BaseHelper.cs | 0 .../Framework/ModHelpers/CommandHelper.cs | 0 .../Framework/ModHelpers/ContentHelper.cs | 0 .../Framework/ModHelpers/ModHelper.cs | 0 .../Framework/ModHelpers/ModRegistryHelper.cs | 0 .../Framework/ModHelpers/ReflectionHelper.cs | 0 .../Framework/ModHelpers/TranslationHelper.cs | 0 .../ModLoading/AssemblyDefinitionResolver.cs | 0 .../ModLoading/AssemblyLoadStatus.cs | 0 .../Framework/ModLoading/AssemblyLoader.cs | 0 .../ModLoading/AssemblyParseResult.cs | 0 .../ModLoading/Finders/EventFinder.cs | 0 .../ModLoading/Finders/FieldFinder.cs | 0 .../ModLoading/Finders/MethodFinder.cs | 0 .../ModLoading/Finders/PropertyFinder.cs | 0 .../ModLoading/Finders/TypeFinder.cs | 0 .../ModLoading/IInstructionHandler.cs | 0 .../IncompatibleInstructionException.cs | 0 .../ModLoading/InstructionHandleResult.cs | 0 .../ModLoading/InvalidModStateException.cs | 0 .../ModLoading/ModDependencyStatus.cs | 0 .../Framework/ModLoading/ModMetadata.cs | 0 .../Framework/ModLoading/ModMetadataStatus.cs | 0 .../Framework/ModLoading/ModResolver.cs | 0 .../Framework/ModLoading/Platform.cs | 0 .../ModLoading/PlatformAssemblyMap.cs | 0 .../Framework/ModLoading/RewriteHelper.cs | 0 .../Rewriters/FieldReplaceRewriter.cs | 0 .../Rewriters/FieldToPropertyRewriter.cs | 0 .../Rewriters/MethodParentRewriter.cs | 0 .../Rewriters/TypeReferenceRewriter.cs | 0 .../Rewriters/VirtualEntryCallRemover.cs | 0 .../Framework/ModRegistry.cs | 0 .../Framework/Models/Manifest.cs | 0 .../Framework/Models/ManifestDependency.cs | 0 .../Framework/Models/ModCompatibility.cs | 0 .../Framework/Models/ModDataID.cs | 0 .../Framework/Models/ModDataRecord.cs | 0 .../Framework/Models/ModStatus.cs | 0 .../Framework/Models/SConfig.cs | 0 .../Framework/Monitor.cs | 0 .../Framework/Reflection/CacheEntry.cs | 0 .../Framework/Reflection/PrivateField.cs | 0 .../Framework/Reflection/PrivateMethod.cs | 0 .../Framework/Reflection/PrivateProperty.cs | 0 .../Framework/Reflection/Reflector.cs | 0 .../Framework/RequestExitDelegate.cs | 0 .../Framework/SContentManager.cs | 0 .../Framework/SGame.cs | 0 .../Framework/Serialisation/JsonHelper.cs | 0 .../Serialisation/SFieldConverter.cs | 0 .../SelectiveStringEnumConverter.cs | 0 .../Framework/Utilities/ContextHash.cs | 0 .../Framework/Utilities/Countdown.cs | 0 .../Framework/WebApiClient.cs | 0 .../IAssetData.cs | 0 .../IAssetDataForDictionary.cs | 0 .../IAssetDataForImage.cs | 0 .../IAssetEditor.cs | 0 .../IAssetInfo.cs | 0 .../IAssetLoader.cs | 0 .../ICommandHelper.cs | 0 .../IContentHelper.cs | 0 .../ICursorPosition.cs | 0 src/{StardewModdingAPI => SMAPI}/IManifest.cs | 0 .../IManifestDependency.cs | 0 src/{StardewModdingAPI => SMAPI}/IMod.cs | 0 .../IModHelper.cs | 0 .../IModLinked.cs | 0 .../IModRegistry.cs | 0 src/{StardewModdingAPI => SMAPI}/IMonitor.cs | 0 .../IPrivateField.cs | 0 .../IPrivateMethod.cs | 0 .../IPrivateProperty.cs | 0 .../IReflectionHelper.cs | 0 .../ISemanticVersion.cs | 0 .../ITranslationHelper.cs | 0 src/{StardewModdingAPI => SMAPI}/LogLevel.cs | 0 .../Metadata/CoreAssets.cs | 0 .../Metadata/InstructionMetadata.cs | 0 src/{StardewModdingAPI => SMAPI}/Mod.cs | 0 src/{StardewModdingAPI => SMAPI}/PatchMode.cs | 0 src/{StardewModdingAPI => SMAPI}/Program.cs | 0 .../Properties/AssemblyInfo.cs | 0 .../SemanticVersion.cs | 0 .../StardewModdingAPI.config.json | 0 .../StardewModdingAPI.csproj | 10 ++-- .../Translation.cs | 0 .../Utilities/SButton.cs | 0 .../Utilities/SDate.cs | 0 src/{StardewModdingAPI => SMAPI}/icon.ico | Bin .../packages.config | 0 .../steam_appid.txt | 0 .../unix-launcher.sh | 0 src/TrainerMod/TrainerMod.csproj | 8 ++-- 205 files changed, 50 insertions(+), 40 deletions(-) rename src/.editorconfig => .editorconfig (100%) rename {src => build}/GlobalAssemblyInfo.cs (100%) rename {src => build}/common.targets (100%) rename {src => build}/prepare-install-package.targets (100%) rename CONTRIBUTING.md => docs/CONTRIBUTING.md (100%) rename LICENSE => docs/LICENSE.md (100%) rename README.md => docs/README.md (100%) rename release-notes.md => docs/release-notes.md (100%) rename src/{StardewModdingAPI.ModBuildConfig => ModBuildConfig}/README.md (100%) rename src/{StardewModdingAPI.ModBuildConfig => ModBuildConfig}/assets/nuget-icon.pdn (100%) rename src/{StardewModdingAPI.ModBuildConfig => ModBuildConfig}/assets/nuget-icon.png (100%) rename src/{StardewModdingAPI.ModBuildConfig => ModBuildConfig}/build/smapi.targets (100%) rename src/{StardewModdingAPI.ModBuildConfig => ModBuildConfig}/package.nuspec (100%) rename src/{StardewModdingAPI.ModBuildConfig => ModBuildConfig}/release-notes.md (100%) rename src/{StardewModdingAPI.AssemblyRewriters => SMAPI.AssemblyRewriters}/Properties/AssemblyInfo.cs (100%) rename src/{StardewModdingAPI.AssemblyRewriters => SMAPI.AssemblyRewriters}/SpriteBatchMethods.cs (100%) rename src/{StardewModdingAPI.AssemblyRewriters => SMAPI.AssemblyRewriters}/StardewModdingAPI.AssemblyRewriters.csproj (92%) rename src/{StardewModdingAPI.Installer => SMAPI.Installer}/Enums/Platform.cs (100%) rename src/{StardewModdingAPI.Installer => SMAPI.Installer}/Enums/ScriptAction.cs (100%) rename src/{StardewModdingAPI.Installer => SMAPI.Installer}/InteractiveInstaller.cs (100%) rename src/{StardewModdingAPI.Installer => SMAPI.Installer}/Program.cs (100%) rename src/{StardewModdingAPI.Installer => SMAPI.Installer}/Properties/AssemblyInfo.cs (100%) rename src/{StardewModdingAPI.Installer => SMAPI.Installer}/StardewModdingAPI.Installer.csproj (91%) rename src/{StardewModdingAPI.Installer => SMAPI.Installer}/readme.txt (100%) rename src/{StardewModdingAPI.Models => SMAPI.Models}/ModInfoModel.cs (100%) rename src/{StardewModdingAPI.Models => SMAPI.Models}/ModSeachModel.cs (100%) rename src/{StardewModdingAPI.Models => SMAPI.Models}/StardewModdingAPI.Models.projitems (100%) rename src/{StardewModdingAPI.Models => SMAPI.Models}/StardewModdingAPI.Models.shproj (100%) rename src/{StardewModdingAPI.Tests => SMAPI.Tests}/Core/ModResolverTests.cs (100%) rename src/{StardewModdingAPI.Tests => SMAPI.Tests}/Core/TranslationTests.cs (100%) rename src/{StardewModdingAPI.Tests => SMAPI.Tests}/Properties/AssemblyInfo.cs (100%) rename src/{StardewModdingAPI.Tests => SMAPI.Tests}/Sample.cs (100%) rename src/{StardewModdingAPI.Tests => SMAPI.Tests}/StardewModdingAPI.Tests.csproj (93%) rename src/{StardewModdingAPI.Tests => SMAPI.Tests}/Utilities/SDateTests.cs (100%) rename src/{StardewModdingAPI.Tests => SMAPI.Tests}/Utilities/SemanticVersionTests.cs (100%) rename src/{StardewModdingAPI.Tests => SMAPI.Tests}/packages.config (100%) rename src/{StardewModdingAPI.Web => SMAPI.Web}/Controllers/ModsController.cs (100%) rename src/{StardewModdingAPI.Web => SMAPI.Web}/Framework/ConfigModels/ModUpdateCheckConfig.cs (100%) rename src/{StardewModdingAPI.Web => SMAPI.Web}/Framework/InternalControllerFeatureProvider.cs (100%) rename src/{StardewModdingAPI.Web => SMAPI.Web}/Framework/ModRepositories/BaseRepository.cs (100%) rename src/{StardewModdingAPI.Web => SMAPI.Web}/Framework/ModRepositories/ChucklefishRepository.cs (100%) rename src/{StardewModdingAPI.Web => SMAPI.Web}/Framework/ModRepositories/GitHubRepository.cs (100%) rename src/{StardewModdingAPI.Web => SMAPI.Web}/Framework/ModRepositories/IModRepository.cs (100%) rename src/{StardewModdingAPI.Web => SMAPI.Web}/Framework/ModRepositories/NexusRepository.cs (100%) rename src/{StardewModdingAPI.Web => SMAPI.Web}/Framework/RewriteSubdomainRule.cs (100%) rename src/{StardewModdingAPI.Web => SMAPI.Web}/Framework/VersionConstraint.cs (100%) rename src/{StardewModdingAPI.Web => SMAPI.Web}/Program.cs (100%) rename src/{StardewModdingAPI.Web => SMAPI.Web}/Properties/AssemblyInfo.cs (100%) rename src/{StardewModdingAPI.Web => SMAPI.Web}/Properties/launchSettings.json (100%) rename src/{StardewModdingAPI.Web => SMAPI.Web}/StardewModdingAPI.Web.csproj (79%) rename src/{StardewModdingAPI.Web => SMAPI.Web}/Startup.cs (100%) rename src/{StardewModdingAPI.Web => SMAPI.Web}/appsettings.Development.json (100%) rename src/{StardewModdingAPI.Web => SMAPI.Web}/appsettings.json (100%) rename src/{StardewModdingAPI.sln => SMAPI.sln} (78%) rename src/{StardewModdingAPI.sln.DotSettings => SMAPI.sln.DotSettings} (100%) rename src/{StardewModdingAPI => SMAPI}/App.config (100%) rename src/{StardewModdingAPI => SMAPI}/Constants.cs (100%) rename src/{StardewModdingAPI => SMAPI}/ContentSource.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Context.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Events/ChangeType.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Events/ContentEvents.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Events/ControlEvents.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Events/EventArgsClickableMenuChanged.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Events/EventArgsClickableMenuClosed.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Events/EventArgsControllerButtonPressed.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Events/EventArgsControllerButtonReleased.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Events/EventArgsControllerTriggerPressed.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Events/EventArgsControllerTriggerReleased.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Events/EventArgsCurrentLocationChanged.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Events/EventArgsGameLocationsChanged.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Events/EventArgsInput.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Events/EventArgsIntChanged.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Events/EventArgsInventoryChanged.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Events/EventArgsKeyPressed.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Events/EventArgsKeyboardStateChanged.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Events/EventArgsLevelUp.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Events/EventArgsLocationObjectsChanged.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Events/EventArgsMineLevelChanged.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Events/EventArgsMouseStateChanged.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Events/EventArgsValueChanged.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Events/GameEvents.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Events/GraphicsEvents.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Events/InputEvents.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Events/ItemStackChange.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Events/LocationEvents.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Events/MenuEvents.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Events/MineEvents.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Events/PlayerEvents.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Events/SaveEvents.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Events/TimeEvents.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/Command.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/CommandManager.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/Content/AssetData.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/Content/AssetDataForDictionary.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/Content/AssetDataForImage.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/Content/AssetDataForObject.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/Content/AssetInfo.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/ContentManagerShim.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/CursorPosition.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/DeprecationLevel.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/DeprecationManager.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/Exceptions/SAssemblyLoadFailedException.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/Exceptions/SContentLoadException.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/Exceptions/SParseException.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/GameVersion.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/IModMetadata.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/InternalExtensions.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/Logging/ConsoleInterceptionManager.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/Logging/InterceptingTextWriter.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/Logging/LogFileManager.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/ModHelpers/BaseHelper.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/ModHelpers/CommandHelper.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/ModHelpers/ContentHelper.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/ModHelpers/ModHelper.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/ModHelpers/ModRegistryHelper.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/ModHelpers/ReflectionHelper.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/ModHelpers/TranslationHelper.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/ModLoading/AssemblyDefinitionResolver.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/ModLoading/AssemblyLoadStatus.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/ModLoading/AssemblyLoader.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/ModLoading/AssemblyParseResult.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/ModLoading/Finders/EventFinder.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/ModLoading/Finders/FieldFinder.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/ModLoading/Finders/MethodFinder.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/ModLoading/Finders/PropertyFinder.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/ModLoading/Finders/TypeFinder.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/ModLoading/IInstructionHandler.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/ModLoading/IncompatibleInstructionException.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/ModLoading/InstructionHandleResult.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/ModLoading/InvalidModStateException.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/ModLoading/ModDependencyStatus.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/ModLoading/ModMetadata.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/ModLoading/ModMetadataStatus.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/ModLoading/ModResolver.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/ModLoading/Platform.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/ModLoading/PlatformAssemblyMap.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/ModLoading/RewriteHelper.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/ModLoading/Rewriters/MethodParentRewriter.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/ModLoading/Rewriters/VirtualEntryCallRemover.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/ModRegistry.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/Models/Manifest.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/Models/ManifestDependency.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/Models/ModCompatibility.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/Models/ModDataID.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/Models/ModDataRecord.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/Models/ModStatus.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/Models/SConfig.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/Monitor.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/Reflection/CacheEntry.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/Reflection/PrivateField.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/Reflection/PrivateMethod.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/Reflection/PrivateProperty.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/Reflection/Reflector.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/RequestExitDelegate.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/SContentManager.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/SGame.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/Serialisation/JsonHelper.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/Serialisation/SFieldConverter.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/Serialisation/SelectiveStringEnumConverter.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/Utilities/ContextHash.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/Utilities/Countdown.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Framework/WebApiClient.cs (100%) rename src/{StardewModdingAPI => SMAPI}/IAssetData.cs (100%) rename src/{StardewModdingAPI => SMAPI}/IAssetDataForDictionary.cs (100%) rename src/{StardewModdingAPI => SMAPI}/IAssetDataForImage.cs (100%) rename src/{StardewModdingAPI => SMAPI}/IAssetEditor.cs (100%) rename src/{StardewModdingAPI => SMAPI}/IAssetInfo.cs (100%) rename src/{StardewModdingAPI => SMAPI}/IAssetLoader.cs (100%) rename src/{StardewModdingAPI => SMAPI}/ICommandHelper.cs (100%) rename src/{StardewModdingAPI => SMAPI}/IContentHelper.cs (100%) rename src/{StardewModdingAPI => SMAPI}/ICursorPosition.cs (100%) rename src/{StardewModdingAPI => SMAPI}/IManifest.cs (100%) rename src/{StardewModdingAPI => SMAPI}/IManifestDependency.cs (100%) rename src/{StardewModdingAPI => SMAPI}/IMod.cs (100%) rename src/{StardewModdingAPI => SMAPI}/IModHelper.cs (100%) rename src/{StardewModdingAPI => SMAPI}/IModLinked.cs (100%) rename src/{StardewModdingAPI => SMAPI}/IModRegistry.cs (100%) rename src/{StardewModdingAPI => SMAPI}/IMonitor.cs (100%) rename src/{StardewModdingAPI => SMAPI}/IPrivateField.cs (100%) rename src/{StardewModdingAPI => SMAPI}/IPrivateMethod.cs (100%) rename src/{StardewModdingAPI => SMAPI}/IPrivateProperty.cs (100%) rename src/{StardewModdingAPI => SMAPI}/IReflectionHelper.cs (100%) rename src/{StardewModdingAPI => SMAPI}/ISemanticVersion.cs (100%) rename src/{StardewModdingAPI => SMAPI}/ITranslationHelper.cs (100%) rename src/{StardewModdingAPI => SMAPI}/LogLevel.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Metadata/CoreAssets.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Metadata/InstructionMetadata.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Mod.cs (100%) rename src/{StardewModdingAPI => SMAPI}/PatchMode.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Program.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Properties/AssemblyInfo.cs (100%) rename src/{StardewModdingAPI => SMAPI}/SemanticVersion.cs (100%) rename src/{StardewModdingAPI => SMAPI}/StardewModdingAPI.config.json (100%) rename src/{StardewModdingAPI => SMAPI}/StardewModdingAPI.csproj (97%) rename src/{StardewModdingAPI => SMAPI}/Translation.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Utilities/SButton.cs (100%) rename src/{StardewModdingAPI => SMAPI}/Utilities/SDate.cs (100%) rename src/{StardewModdingAPI => SMAPI}/icon.ico (100%) rename src/{StardewModdingAPI => SMAPI}/packages.config (100%) rename src/{StardewModdingAPI => SMAPI}/steam_appid.txt (100%) rename src/{StardewModdingAPI => SMAPI}/unix-launcher.sh (100%) diff --git a/src/.editorconfig b/.editorconfig similarity index 100% rename from src/.editorconfig rename to .editorconfig diff --git a/src/GlobalAssemblyInfo.cs b/build/GlobalAssemblyInfo.cs similarity index 100% rename from src/GlobalAssemblyInfo.cs rename to build/GlobalAssemblyInfo.cs diff --git a/src/common.targets b/build/common.targets similarity index 100% rename from src/common.targets rename to build/common.targets diff --git a/src/prepare-install-package.targets b/build/prepare-install-package.targets similarity index 100% rename from src/prepare-install-package.targets rename to build/prepare-install-package.targets diff --git a/CONTRIBUTING.md b/docs/CONTRIBUTING.md similarity index 100% rename from CONTRIBUTING.md rename to docs/CONTRIBUTING.md diff --git a/LICENSE b/docs/LICENSE.md similarity index 100% rename from LICENSE rename to docs/LICENSE.md diff --git a/README.md b/docs/README.md similarity index 100% rename from README.md rename to docs/README.md diff --git a/release-notes.md b/docs/release-notes.md similarity index 100% rename from release-notes.md rename to docs/release-notes.md diff --git a/src/StardewModdingAPI.ModBuildConfig/README.md b/src/ModBuildConfig/README.md similarity index 100% rename from src/StardewModdingAPI.ModBuildConfig/README.md rename to src/ModBuildConfig/README.md diff --git a/src/StardewModdingAPI.ModBuildConfig/assets/nuget-icon.pdn b/src/ModBuildConfig/assets/nuget-icon.pdn similarity index 100% rename from src/StardewModdingAPI.ModBuildConfig/assets/nuget-icon.pdn rename to src/ModBuildConfig/assets/nuget-icon.pdn diff --git a/src/StardewModdingAPI.ModBuildConfig/assets/nuget-icon.png b/src/ModBuildConfig/assets/nuget-icon.png similarity index 100% rename from src/StardewModdingAPI.ModBuildConfig/assets/nuget-icon.png rename to src/ModBuildConfig/assets/nuget-icon.png diff --git a/src/StardewModdingAPI.ModBuildConfig/build/smapi.targets b/src/ModBuildConfig/build/smapi.targets similarity index 100% rename from src/StardewModdingAPI.ModBuildConfig/build/smapi.targets rename to src/ModBuildConfig/build/smapi.targets diff --git a/src/StardewModdingAPI.ModBuildConfig/package.nuspec b/src/ModBuildConfig/package.nuspec similarity index 100% rename from src/StardewModdingAPI.ModBuildConfig/package.nuspec rename to src/ModBuildConfig/package.nuspec diff --git a/src/StardewModdingAPI.ModBuildConfig/release-notes.md b/src/ModBuildConfig/release-notes.md similarity index 100% rename from src/StardewModdingAPI.ModBuildConfig/release-notes.md rename to src/ModBuildConfig/release-notes.md diff --git a/src/StardewModdingAPI.AssemblyRewriters/Properties/AssemblyInfo.cs b/src/SMAPI.AssemblyRewriters/Properties/AssemblyInfo.cs similarity index 100% rename from src/StardewModdingAPI.AssemblyRewriters/Properties/AssemblyInfo.cs rename to src/SMAPI.AssemblyRewriters/Properties/AssemblyInfo.cs diff --git a/src/StardewModdingAPI.AssemblyRewriters/SpriteBatchMethods.cs b/src/SMAPI.AssemblyRewriters/SpriteBatchMethods.cs similarity index 100% rename from src/StardewModdingAPI.AssemblyRewriters/SpriteBatchMethods.cs rename to src/SMAPI.AssemblyRewriters/SpriteBatchMethods.cs diff --git a/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj b/src/SMAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj similarity index 92% rename from src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj rename to src/SMAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj index c8b03086..651b822d 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj +++ b/src/SMAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj @@ -1,4 +1,4 @@ - + @@ -33,12 +33,12 @@ - + Properties\GlobalAssemblyInfo.cs - + \ No newline at end of file diff --git a/src/StardewModdingAPI.Installer/Enums/Platform.cs b/src/SMAPI.Installer/Enums/Platform.cs similarity index 100% rename from src/StardewModdingAPI.Installer/Enums/Platform.cs rename to src/SMAPI.Installer/Enums/Platform.cs diff --git a/src/StardewModdingAPI.Installer/Enums/ScriptAction.cs b/src/SMAPI.Installer/Enums/ScriptAction.cs similarity index 100% rename from src/StardewModdingAPI.Installer/Enums/ScriptAction.cs rename to src/SMAPI.Installer/Enums/ScriptAction.cs diff --git a/src/StardewModdingAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs similarity index 100% rename from src/StardewModdingAPI.Installer/InteractiveInstaller.cs rename to src/SMAPI.Installer/InteractiveInstaller.cs diff --git a/src/StardewModdingAPI.Installer/Program.cs b/src/SMAPI.Installer/Program.cs similarity index 100% rename from src/StardewModdingAPI.Installer/Program.cs rename to src/SMAPI.Installer/Program.cs diff --git a/src/StardewModdingAPI.Installer/Properties/AssemblyInfo.cs b/src/SMAPI.Installer/Properties/AssemblyInfo.cs similarity index 100% rename from src/StardewModdingAPI.Installer/Properties/AssemblyInfo.cs rename to src/SMAPI.Installer/Properties/AssemblyInfo.cs diff --git a/src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj b/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj similarity index 91% rename from src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj rename to src/SMAPI.Installer/StardewModdingAPI.Installer.csproj index 58ce519c..f8e368a4 100644 --- a/src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj +++ b/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj @@ -1,4 +1,4 @@ - + @@ -36,7 +36,7 @@ - + Properties\GlobalAssemblyInfo.cs @@ -51,6 +51,6 @@ - - + + \ No newline at end of file diff --git a/src/StardewModdingAPI.Installer/readme.txt b/src/SMAPI.Installer/readme.txt similarity index 100% rename from src/StardewModdingAPI.Installer/readme.txt rename to src/SMAPI.Installer/readme.txt diff --git a/src/StardewModdingAPI.Models/ModInfoModel.cs b/src/SMAPI.Models/ModInfoModel.cs similarity index 100% rename from src/StardewModdingAPI.Models/ModInfoModel.cs rename to src/SMAPI.Models/ModInfoModel.cs diff --git a/src/StardewModdingAPI.Models/ModSeachModel.cs b/src/SMAPI.Models/ModSeachModel.cs similarity index 100% rename from src/StardewModdingAPI.Models/ModSeachModel.cs rename to src/SMAPI.Models/ModSeachModel.cs diff --git a/src/StardewModdingAPI.Models/StardewModdingAPI.Models.projitems b/src/SMAPI.Models/StardewModdingAPI.Models.projitems similarity index 100% rename from src/StardewModdingAPI.Models/StardewModdingAPI.Models.projitems rename to src/SMAPI.Models/StardewModdingAPI.Models.projitems diff --git a/src/StardewModdingAPI.Models/StardewModdingAPI.Models.shproj b/src/SMAPI.Models/StardewModdingAPI.Models.shproj similarity index 100% rename from src/StardewModdingAPI.Models/StardewModdingAPI.Models.shproj rename to src/SMAPI.Models/StardewModdingAPI.Models.shproj diff --git a/src/StardewModdingAPI.Tests/Core/ModResolverTests.cs b/src/SMAPI.Tests/Core/ModResolverTests.cs similarity index 100% rename from src/StardewModdingAPI.Tests/Core/ModResolverTests.cs rename to src/SMAPI.Tests/Core/ModResolverTests.cs diff --git a/src/StardewModdingAPI.Tests/Core/TranslationTests.cs b/src/SMAPI.Tests/Core/TranslationTests.cs similarity index 100% rename from src/StardewModdingAPI.Tests/Core/TranslationTests.cs rename to src/SMAPI.Tests/Core/TranslationTests.cs diff --git a/src/StardewModdingAPI.Tests/Properties/AssemblyInfo.cs b/src/SMAPI.Tests/Properties/AssemblyInfo.cs similarity index 100% rename from src/StardewModdingAPI.Tests/Properties/AssemblyInfo.cs rename to src/SMAPI.Tests/Properties/AssemblyInfo.cs diff --git a/src/StardewModdingAPI.Tests/Sample.cs b/src/SMAPI.Tests/Sample.cs similarity index 100% rename from src/StardewModdingAPI.Tests/Sample.cs rename to src/SMAPI.Tests/Sample.cs diff --git a/src/StardewModdingAPI.Tests/StardewModdingAPI.Tests.csproj b/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj similarity index 93% rename from src/StardewModdingAPI.Tests/StardewModdingAPI.Tests.csproj rename to src/SMAPI.Tests/StardewModdingAPI.Tests.csproj index 41525bcb..42c3318f 100644 --- a/src/StardewModdingAPI.Tests/StardewModdingAPI.Tests.csproj +++ b/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj @@ -1,4 +1,4 @@ - + @@ -45,7 +45,7 @@ - + Properties\GlobalAssemblyInfo.cs @@ -59,11 +59,11 @@ - + {f1a573b0-f436-472c-ae29-0b91ea6b9f8f} StardewModdingAPI - + \ No newline at end of file diff --git a/src/StardewModdingAPI.Tests/Utilities/SDateTests.cs b/src/SMAPI.Tests/Utilities/SDateTests.cs similarity index 100% rename from src/StardewModdingAPI.Tests/Utilities/SDateTests.cs rename to src/SMAPI.Tests/Utilities/SDateTests.cs diff --git a/src/StardewModdingAPI.Tests/Utilities/SemanticVersionTests.cs b/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs similarity index 100% rename from src/StardewModdingAPI.Tests/Utilities/SemanticVersionTests.cs rename to src/SMAPI.Tests/Utilities/SemanticVersionTests.cs diff --git a/src/StardewModdingAPI.Tests/packages.config b/src/SMAPI.Tests/packages.config similarity index 100% rename from src/StardewModdingAPI.Tests/packages.config rename to src/SMAPI.Tests/packages.config diff --git a/src/StardewModdingAPI.Web/Controllers/ModsController.cs b/src/SMAPI.Web/Controllers/ModsController.cs similarity index 100% rename from src/StardewModdingAPI.Web/Controllers/ModsController.cs rename to src/SMAPI.Web/Controllers/ModsController.cs diff --git a/src/StardewModdingAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs similarity index 100% rename from src/StardewModdingAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs rename to src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs diff --git a/src/StardewModdingAPI.Web/Framework/InternalControllerFeatureProvider.cs b/src/SMAPI.Web/Framework/InternalControllerFeatureProvider.cs similarity index 100% rename from src/StardewModdingAPI.Web/Framework/InternalControllerFeatureProvider.cs rename to src/SMAPI.Web/Framework/InternalControllerFeatureProvider.cs diff --git a/src/StardewModdingAPI.Web/Framework/ModRepositories/BaseRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs similarity index 100% rename from src/StardewModdingAPI.Web/Framework/ModRepositories/BaseRepository.cs rename to src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs diff --git a/src/StardewModdingAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs similarity index 100% rename from src/StardewModdingAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs rename to src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs diff --git a/src/StardewModdingAPI.Web/Framework/ModRepositories/GitHubRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs similarity index 100% rename from src/StardewModdingAPI.Web/Framework/ModRepositories/GitHubRepository.cs rename to src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs diff --git a/src/StardewModdingAPI.Web/Framework/ModRepositories/IModRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs similarity index 100% rename from src/StardewModdingAPI.Web/Framework/ModRepositories/IModRepository.cs rename to src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs diff --git a/src/StardewModdingAPI.Web/Framework/ModRepositories/NexusRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs similarity index 100% rename from src/StardewModdingAPI.Web/Framework/ModRepositories/NexusRepository.cs rename to src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs diff --git a/src/StardewModdingAPI.Web/Framework/RewriteSubdomainRule.cs b/src/SMAPI.Web/Framework/RewriteSubdomainRule.cs similarity index 100% rename from src/StardewModdingAPI.Web/Framework/RewriteSubdomainRule.cs rename to src/SMAPI.Web/Framework/RewriteSubdomainRule.cs diff --git a/src/StardewModdingAPI.Web/Framework/VersionConstraint.cs b/src/SMAPI.Web/Framework/VersionConstraint.cs similarity index 100% rename from src/StardewModdingAPI.Web/Framework/VersionConstraint.cs rename to src/SMAPI.Web/Framework/VersionConstraint.cs diff --git a/src/StardewModdingAPI.Web/Program.cs b/src/SMAPI.Web/Program.cs similarity index 100% rename from src/StardewModdingAPI.Web/Program.cs rename to src/SMAPI.Web/Program.cs diff --git a/src/StardewModdingAPI.Web/Properties/AssemblyInfo.cs b/src/SMAPI.Web/Properties/AssemblyInfo.cs similarity index 100% rename from src/StardewModdingAPI.Web/Properties/AssemblyInfo.cs rename to src/SMAPI.Web/Properties/AssemblyInfo.cs diff --git a/src/StardewModdingAPI.Web/Properties/launchSettings.json b/src/SMAPI.Web/Properties/launchSettings.json similarity index 100% rename from src/StardewModdingAPI.Web/Properties/launchSettings.json rename to src/SMAPI.Web/Properties/launchSettings.json diff --git a/src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj b/src/SMAPI.Web/StardewModdingAPI.Web.csproj similarity index 79% rename from src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj rename to src/SMAPI.Web/StardewModdingAPI.Web.csproj index 746b1a69..6b1d0687 100644 --- a/src/StardewModdingAPI.Web/StardewModdingAPI.Web.csproj +++ b/src/SMAPI.Web/StardewModdingAPI.Web.csproj @@ -1,4 +1,4 @@ - + netcoreapp2.0 @@ -6,7 +6,7 @@ - + @@ -21,6 +21,6 @@ - + diff --git a/src/StardewModdingAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs similarity index 100% rename from src/StardewModdingAPI.Web/Startup.cs rename to src/SMAPI.Web/Startup.cs diff --git a/src/StardewModdingAPI.Web/appsettings.Development.json b/src/SMAPI.Web/appsettings.Development.json similarity index 100% rename from src/StardewModdingAPI.Web/appsettings.Development.json rename to src/SMAPI.Web/appsettings.Development.json diff --git a/src/StardewModdingAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json similarity index 100% rename from src/StardewModdingAPI.Web/appsettings.json rename to src/SMAPI.Web/appsettings.json diff --git a/src/StardewModdingAPI.sln b/src/SMAPI.sln similarity index 78% rename from src/StardewModdingAPI.sln rename to src/SMAPI.sln index 5e8a2c93..5936ff43 100644 --- a/src/StardewModdingAPI.sln +++ b/src/SMAPI.sln @@ -5,42 +5,50 @@ VisualStudioVersion = 15.0.26730.16 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrainerMod", "TrainerMod\TrainerMod.csproj", "{28480467-1A48-46A7-99F8-236D95225359}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI", "StardewModdingAPI\StardewModdingAPI.csproj", "{F1A573B0-F436-472C-AE29-0B91EA6B9F8F}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI", "SMAPI\StardewModdingAPI.csproj", "{F1A573B0-F436-472C-AE29-0B91EA6B9F8F}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "metadata", "metadata", "{86C452BE-D2D8-45B4-B63F-E329EB06CEDA}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".root", ".root", "{86C452BE-D2D8-45B4-B63F-E329EB06CEDA}" ProjectSection(SolutionItems) = preProject - .editorconfig = .editorconfig + ..\.editorconfig = ..\.editorconfig ..\.gitattributes = ..\.gitattributes ..\.gitignore = ..\.gitignore - common.targets = common.targets - ..\CONTRIBUTING.md = ..\CONTRIBUTING.md - GlobalAssemblyInfo.cs = GlobalAssemblyInfo.cs - ..\LICENSE = ..\LICENSE - prepare-install-package.targets = prepare-install-package.targets - ..\README.md = ..\README.md - ..\release-notes.md = ..\release-notes.md EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Installer", "StardewModdingAPI.Installer\StardewModdingAPI.Installer.csproj", "{443DDF81-6AAF-420A-A610-3459F37E5575}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Installer", "SMAPI.Installer\StardewModdingAPI.Installer.csproj", "{443DDF81-6AAF-420A-A610-3459F37E5575}" ProjectSection(ProjectDependencies) = postProject {28480467-1A48-46A7-99F8-236D95225359} = {28480467-1A48-46A7-99F8-236D95225359} {F1A573B0-F436-472C-AE29-0B91EA6B9F8F} = {F1A573B0-F436-472C-AE29-0B91EA6B9F8F} EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.AssemblyRewriters", "StardewModdingAPI.AssemblyRewriters\StardewModdingAPI.AssemblyRewriters.csproj", "{10DB0676-9FC1-4771-A2C8-E2519F091E49}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.AssemblyRewriters", "SMAPI.AssemblyRewriters\StardewModdingAPI.AssemblyRewriters.csproj", "{10DB0676-9FC1-4771-A2C8-E2519F091E49}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Tests", "StardewModdingAPI.Tests\StardewModdingAPI.Tests.csproj", "{36CCB19E-92EB-48C7-9615-98EEFD45109B}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Tests", "SMAPI.Tests\StardewModdingAPI.Tests.csproj", "{36CCB19E-92EB-48C7-9615-98EEFD45109B}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StardewModdingAPI.Web", "StardewModdingAPI.Web\StardewModdingAPI.Web.csproj", "{A308F679-51A3-4006-92D5-BAEC7EBD01A1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StardewModdingAPI.Web", "SMAPI.Web\StardewModdingAPI.Web.csproj", "{A308F679-51A3-4006-92D5-BAEC7EBD01A1}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Internal", "Internal", "{82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11}" EndProject -Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "StardewModdingAPI.Models", "StardewModdingAPI.Models\StardewModdingAPI.Models.shproj", "{2AA02FB6-FF03-41CF-A215-2EE60AB4F5DC}" +Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "StardewModdingAPI.Models", "SMAPI.Models\StardewModdingAPI.Models.shproj", "{2AA02FB6-FF03-41CF-A215-2EE60AB4F5DC}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{EB35A917-67B9-4EFA-8DFC-4FB49B3949BB}" + ProjectSection(SolutionItems) = preProject + ..\docs\CONTRIBUTING.md = ..\docs\CONTRIBUTING.md + ..\docs\LICENSE.md = ..\docs\LICENSE.md + ..\docs\README.md = ..\docs\README.md + ..\docs\release-notes.md = ..\docs\release-notes.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{09CF91E5-5BAB-4650-A200-E5EA9A633046}" + ProjectSection(SolutionItems) = preProject + ..\build\common.targets = ..\build\common.targets + ..\build\GlobalAssemblyInfo.cs = ..\build\GlobalAssemblyInfo.cs + ..\build\prepare-install-package.targets = ..\build\prepare-install-package.targets + EndProjectSection EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution - StardewModdingAPI.Models\StardewModdingAPI.Models.projitems*{2aa02fb6-ff03-41cf-a215-2ee60ab4f5dc}*SharedItemsImports = 13 - StardewModdingAPI.Models\StardewModdingAPI.Models.projitems*{f1a573b0-f436-472c-ae29-0b91ea6b9f8f}*SharedItemsImports = 4 + SMAPI.Models\StardewModdingAPI.Models.projitems*{2aa02fb6-ff03-41cf-a215-2ee60ab4f5dc}*SharedItemsImports = 13 + SMAPI.Models\StardewModdingAPI.Models.projitems*{f1a573b0-f436-472c-ae29-0b91ea6b9f8f}*SharedItemsImports = 4 EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -121,6 +129,8 @@ Global {10DB0676-9FC1-4771-A2C8-E2519F091E49} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} {36CCB19E-92EB-48C7-9615-98EEFD45109B} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} {2AA02FB6-FF03-41CF-A215-2EE60AB4F5DC} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} + {EB35A917-67B9-4EFA-8DFC-4FB49B3949BB} = {86C452BE-D2D8-45B4-B63F-E329EB06CEDA} + {09CF91E5-5BAB-4650-A200-E5EA9A633046} = {86C452BE-D2D8-45B4-B63F-E329EB06CEDA} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {70143042-A862-47A8-A677-7C819DDC90DC} diff --git a/src/StardewModdingAPI.sln.DotSettings b/src/SMAPI.sln.DotSettings similarity index 100% rename from src/StardewModdingAPI.sln.DotSettings rename to src/SMAPI.sln.DotSettings diff --git a/src/StardewModdingAPI/App.config b/src/SMAPI/App.config similarity index 100% rename from src/StardewModdingAPI/App.config rename to src/SMAPI/App.config diff --git a/src/StardewModdingAPI/Constants.cs b/src/SMAPI/Constants.cs similarity index 100% rename from src/StardewModdingAPI/Constants.cs rename to src/SMAPI/Constants.cs diff --git a/src/StardewModdingAPI/ContentSource.cs b/src/SMAPI/ContentSource.cs similarity index 100% rename from src/StardewModdingAPI/ContentSource.cs rename to src/SMAPI/ContentSource.cs diff --git a/src/StardewModdingAPI/Context.cs b/src/SMAPI/Context.cs similarity index 100% rename from src/StardewModdingAPI/Context.cs rename to src/SMAPI/Context.cs diff --git a/src/StardewModdingAPI/Events/ChangeType.cs b/src/SMAPI/Events/ChangeType.cs similarity index 100% rename from src/StardewModdingAPI/Events/ChangeType.cs rename to src/SMAPI/Events/ChangeType.cs diff --git a/src/StardewModdingAPI/Events/ContentEvents.cs b/src/SMAPI/Events/ContentEvents.cs similarity index 100% rename from src/StardewModdingAPI/Events/ContentEvents.cs rename to src/SMAPI/Events/ContentEvents.cs diff --git a/src/StardewModdingAPI/Events/ControlEvents.cs b/src/SMAPI/Events/ControlEvents.cs similarity index 100% rename from src/StardewModdingAPI/Events/ControlEvents.cs rename to src/SMAPI/Events/ControlEvents.cs diff --git a/src/StardewModdingAPI/Events/EventArgsClickableMenuChanged.cs b/src/SMAPI/Events/EventArgsClickableMenuChanged.cs similarity index 100% rename from src/StardewModdingAPI/Events/EventArgsClickableMenuChanged.cs rename to src/SMAPI/Events/EventArgsClickableMenuChanged.cs diff --git a/src/StardewModdingAPI/Events/EventArgsClickableMenuClosed.cs b/src/SMAPI/Events/EventArgsClickableMenuClosed.cs similarity index 100% rename from src/StardewModdingAPI/Events/EventArgsClickableMenuClosed.cs rename to src/SMAPI/Events/EventArgsClickableMenuClosed.cs diff --git a/src/StardewModdingAPI/Events/EventArgsControllerButtonPressed.cs b/src/SMAPI/Events/EventArgsControllerButtonPressed.cs similarity index 100% rename from src/StardewModdingAPI/Events/EventArgsControllerButtonPressed.cs rename to src/SMAPI/Events/EventArgsControllerButtonPressed.cs diff --git a/src/StardewModdingAPI/Events/EventArgsControllerButtonReleased.cs b/src/SMAPI/Events/EventArgsControllerButtonReleased.cs similarity index 100% rename from src/StardewModdingAPI/Events/EventArgsControllerButtonReleased.cs rename to src/SMAPI/Events/EventArgsControllerButtonReleased.cs diff --git a/src/StardewModdingAPI/Events/EventArgsControllerTriggerPressed.cs b/src/SMAPI/Events/EventArgsControllerTriggerPressed.cs similarity index 100% rename from src/StardewModdingAPI/Events/EventArgsControllerTriggerPressed.cs rename to src/SMAPI/Events/EventArgsControllerTriggerPressed.cs diff --git a/src/StardewModdingAPI/Events/EventArgsControllerTriggerReleased.cs b/src/SMAPI/Events/EventArgsControllerTriggerReleased.cs similarity index 100% rename from src/StardewModdingAPI/Events/EventArgsControllerTriggerReleased.cs rename to src/SMAPI/Events/EventArgsControllerTriggerReleased.cs diff --git a/src/StardewModdingAPI/Events/EventArgsCurrentLocationChanged.cs b/src/SMAPI/Events/EventArgsCurrentLocationChanged.cs similarity index 100% rename from src/StardewModdingAPI/Events/EventArgsCurrentLocationChanged.cs rename to src/SMAPI/Events/EventArgsCurrentLocationChanged.cs diff --git a/src/StardewModdingAPI/Events/EventArgsGameLocationsChanged.cs b/src/SMAPI/Events/EventArgsGameLocationsChanged.cs similarity index 100% rename from src/StardewModdingAPI/Events/EventArgsGameLocationsChanged.cs rename to src/SMAPI/Events/EventArgsGameLocationsChanged.cs diff --git a/src/StardewModdingAPI/Events/EventArgsInput.cs b/src/SMAPI/Events/EventArgsInput.cs similarity index 100% rename from src/StardewModdingAPI/Events/EventArgsInput.cs rename to src/SMAPI/Events/EventArgsInput.cs diff --git a/src/StardewModdingAPI/Events/EventArgsIntChanged.cs b/src/SMAPI/Events/EventArgsIntChanged.cs similarity index 100% rename from src/StardewModdingAPI/Events/EventArgsIntChanged.cs rename to src/SMAPI/Events/EventArgsIntChanged.cs diff --git a/src/StardewModdingAPI/Events/EventArgsInventoryChanged.cs b/src/SMAPI/Events/EventArgsInventoryChanged.cs similarity index 100% rename from src/StardewModdingAPI/Events/EventArgsInventoryChanged.cs rename to src/SMAPI/Events/EventArgsInventoryChanged.cs diff --git a/src/StardewModdingAPI/Events/EventArgsKeyPressed.cs b/src/SMAPI/Events/EventArgsKeyPressed.cs similarity index 100% rename from src/StardewModdingAPI/Events/EventArgsKeyPressed.cs rename to src/SMAPI/Events/EventArgsKeyPressed.cs diff --git a/src/StardewModdingAPI/Events/EventArgsKeyboardStateChanged.cs b/src/SMAPI/Events/EventArgsKeyboardStateChanged.cs similarity index 100% rename from src/StardewModdingAPI/Events/EventArgsKeyboardStateChanged.cs rename to src/SMAPI/Events/EventArgsKeyboardStateChanged.cs diff --git a/src/StardewModdingAPI/Events/EventArgsLevelUp.cs b/src/SMAPI/Events/EventArgsLevelUp.cs similarity index 100% rename from src/StardewModdingAPI/Events/EventArgsLevelUp.cs rename to src/SMAPI/Events/EventArgsLevelUp.cs diff --git a/src/StardewModdingAPI/Events/EventArgsLocationObjectsChanged.cs b/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs similarity index 100% rename from src/StardewModdingAPI/Events/EventArgsLocationObjectsChanged.cs rename to src/SMAPI/Events/EventArgsLocationObjectsChanged.cs diff --git a/src/StardewModdingAPI/Events/EventArgsMineLevelChanged.cs b/src/SMAPI/Events/EventArgsMineLevelChanged.cs similarity index 100% rename from src/StardewModdingAPI/Events/EventArgsMineLevelChanged.cs rename to src/SMAPI/Events/EventArgsMineLevelChanged.cs diff --git a/src/StardewModdingAPI/Events/EventArgsMouseStateChanged.cs b/src/SMAPI/Events/EventArgsMouseStateChanged.cs similarity index 100% rename from src/StardewModdingAPI/Events/EventArgsMouseStateChanged.cs rename to src/SMAPI/Events/EventArgsMouseStateChanged.cs diff --git a/src/StardewModdingAPI/Events/EventArgsValueChanged.cs b/src/SMAPI/Events/EventArgsValueChanged.cs similarity index 100% rename from src/StardewModdingAPI/Events/EventArgsValueChanged.cs rename to src/SMAPI/Events/EventArgsValueChanged.cs diff --git a/src/StardewModdingAPI/Events/GameEvents.cs b/src/SMAPI/Events/GameEvents.cs similarity index 100% rename from src/StardewModdingAPI/Events/GameEvents.cs rename to src/SMAPI/Events/GameEvents.cs diff --git a/src/StardewModdingAPI/Events/GraphicsEvents.cs b/src/SMAPI/Events/GraphicsEvents.cs similarity index 100% rename from src/StardewModdingAPI/Events/GraphicsEvents.cs rename to src/SMAPI/Events/GraphicsEvents.cs diff --git a/src/StardewModdingAPI/Events/InputEvents.cs b/src/SMAPI/Events/InputEvents.cs similarity index 100% rename from src/StardewModdingAPI/Events/InputEvents.cs rename to src/SMAPI/Events/InputEvents.cs diff --git a/src/StardewModdingAPI/Events/ItemStackChange.cs b/src/SMAPI/Events/ItemStackChange.cs similarity index 100% rename from src/StardewModdingAPI/Events/ItemStackChange.cs rename to src/SMAPI/Events/ItemStackChange.cs diff --git a/src/StardewModdingAPI/Events/LocationEvents.cs b/src/SMAPI/Events/LocationEvents.cs similarity index 100% rename from src/StardewModdingAPI/Events/LocationEvents.cs rename to src/SMAPI/Events/LocationEvents.cs diff --git a/src/StardewModdingAPI/Events/MenuEvents.cs b/src/SMAPI/Events/MenuEvents.cs similarity index 100% rename from src/StardewModdingAPI/Events/MenuEvents.cs rename to src/SMAPI/Events/MenuEvents.cs diff --git a/src/StardewModdingAPI/Events/MineEvents.cs b/src/SMAPI/Events/MineEvents.cs similarity index 100% rename from src/StardewModdingAPI/Events/MineEvents.cs rename to src/SMAPI/Events/MineEvents.cs diff --git a/src/StardewModdingAPI/Events/PlayerEvents.cs b/src/SMAPI/Events/PlayerEvents.cs similarity index 100% rename from src/StardewModdingAPI/Events/PlayerEvents.cs rename to src/SMAPI/Events/PlayerEvents.cs diff --git a/src/StardewModdingAPI/Events/SaveEvents.cs b/src/SMAPI/Events/SaveEvents.cs similarity index 100% rename from src/StardewModdingAPI/Events/SaveEvents.cs rename to src/SMAPI/Events/SaveEvents.cs diff --git a/src/StardewModdingAPI/Events/TimeEvents.cs b/src/SMAPI/Events/TimeEvents.cs similarity index 100% rename from src/StardewModdingAPI/Events/TimeEvents.cs rename to src/SMAPI/Events/TimeEvents.cs diff --git a/src/StardewModdingAPI/Framework/Command.cs b/src/SMAPI/Framework/Command.cs similarity index 100% rename from src/StardewModdingAPI/Framework/Command.cs rename to src/SMAPI/Framework/Command.cs diff --git a/src/StardewModdingAPI/Framework/CommandManager.cs b/src/SMAPI/Framework/CommandManager.cs similarity index 100% rename from src/StardewModdingAPI/Framework/CommandManager.cs rename to src/SMAPI/Framework/CommandManager.cs diff --git a/src/StardewModdingAPI/Framework/Content/AssetData.cs b/src/SMAPI/Framework/Content/AssetData.cs similarity index 100% rename from src/StardewModdingAPI/Framework/Content/AssetData.cs rename to src/SMAPI/Framework/Content/AssetData.cs diff --git a/src/StardewModdingAPI/Framework/Content/AssetDataForDictionary.cs b/src/SMAPI/Framework/Content/AssetDataForDictionary.cs similarity index 100% rename from src/StardewModdingAPI/Framework/Content/AssetDataForDictionary.cs rename to src/SMAPI/Framework/Content/AssetDataForDictionary.cs diff --git a/src/StardewModdingAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs similarity index 100% rename from src/StardewModdingAPI/Framework/Content/AssetDataForImage.cs rename to src/SMAPI/Framework/Content/AssetDataForImage.cs diff --git a/src/StardewModdingAPI/Framework/Content/AssetDataForObject.cs b/src/SMAPI/Framework/Content/AssetDataForObject.cs similarity index 100% rename from src/StardewModdingAPI/Framework/Content/AssetDataForObject.cs rename to src/SMAPI/Framework/Content/AssetDataForObject.cs diff --git a/src/StardewModdingAPI/Framework/Content/AssetInfo.cs b/src/SMAPI/Framework/Content/AssetInfo.cs similarity index 100% rename from src/StardewModdingAPI/Framework/Content/AssetInfo.cs rename to src/SMAPI/Framework/Content/AssetInfo.cs diff --git a/src/StardewModdingAPI/Framework/ContentManagerShim.cs b/src/SMAPI/Framework/ContentManagerShim.cs similarity index 100% rename from src/StardewModdingAPI/Framework/ContentManagerShim.cs rename to src/SMAPI/Framework/ContentManagerShim.cs diff --git a/src/StardewModdingAPI/Framework/CursorPosition.cs b/src/SMAPI/Framework/CursorPosition.cs similarity index 100% rename from src/StardewModdingAPI/Framework/CursorPosition.cs rename to src/SMAPI/Framework/CursorPosition.cs diff --git a/src/StardewModdingAPI/Framework/DeprecationLevel.cs b/src/SMAPI/Framework/DeprecationLevel.cs similarity index 100% rename from src/StardewModdingAPI/Framework/DeprecationLevel.cs rename to src/SMAPI/Framework/DeprecationLevel.cs diff --git a/src/StardewModdingAPI/Framework/DeprecationManager.cs b/src/SMAPI/Framework/DeprecationManager.cs similarity index 100% rename from src/StardewModdingAPI/Framework/DeprecationManager.cs rename to src/SMAPI/Framework/DeprecationManager.cs diff --git a/src/StardewModdingAPI/Framework/Exceptions/SAssemblyLoadFailedException.cs b/src/SMAPI/Framework/Exceptions/SAssemblyLoadFailedException.cs similarity index 100% rename from src/StardewModdingAPI/Framework/Exceptions/SAssemblyLoadFailedException.cs rename to src/SMAPI/Framework/Exceptions/SAssemblyLoadFailedException.cs diff --git a/src/StardewModdingAPI/Framework/Exceptions/SContentLoadException.cs b/src/SMAPI/Framework/Exceptions/SContentLoadException.cs similarity index 100% rename from src/StardewModdingAPI/Framework/Exceptions/SContentLoadException.cs rename to src/SMAPI/Framework/Exceptions/SContentLoadException.cs diff --git a/src/StardewModdingAPI/Framework/Exceptions/SParseException.cs b/src/SMAPI/Framework/Exceptions/SParseException.cs similarity index 100% rename from src/StardewModdingAPI/Framework/Exceptions/SParseException.cs rename to src/SMAPI/Framework/Exceptions/SParseException.cs diff --git a/src/StardewModdingAPI/Framework/GameVersion.cs b/src/SMAPI/Framework/GameVersion.cs similarity index 100% rename from src/StardewModdingAPI/Framework/GameVersion.cs rename to src/SMAPI/Framework/GameVersion.cs diff --git a/src/StardewModdingAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs similarity index 100% rename from src/StardewModdingAPI/Framework/IModMetadata.cs rename to src/SMAPI/Framework/IModMetadata.cs diff --git a/src/StardewModdingAPI/Framework/InternalExtensions.cs b/src/SMAPI/Framework/InternalExtensions.cs similarity index 100% rename from src/StardewModdingAPI/Framework/InternalExtensions.cs rename to src/SMAPI/Framework/InternalExtensions.cs diff --git a/src/StardewModdingAPI/Framework/Logging/ConsoleInterceptionManager.cs b/src/SMAPI/Framework/Logging/ConsoleInterceptionManager.cs similarity index 100% rename from src/StardewModdingAPI/Framework/Logging/ConsoleInterceptionManager.cs rename to src/SMAPI/Framework/Logging/ConsoleInterceptionManager.cs diff --git a/src/StardewModdingAPI/Framework/Logging/InterceptingTextWriter.cs b/src/SMAPI/Framework/Logging/InterceptingTextWriter.cs similarity index 100% rename from src/StardewModdingAPI/Framework/Logging/InterceptingTextWriter.cs rename to src/SMAPI/Framework/Logging/InterceptingTextWriter.cs diff --git a/src/StardewModdingAPI/Framework/Logging/LogFileManager.cs b/src/SMAPI/Framework/Logging/LogFileManager.cs similarity index 100% rename from src/StardewModdingAPI/Framework/Logging/LogFileManager.cs rename to src/SMAPI/Framework/Logging/LogFileManager.cs diff --git a/src/StardewModdingAPI/Framework/ModHelpers/BaseHelper.cs b/src/SMAPI/Framework/ModHelpers/BaseHelper.cs similarity index 100% rename from src/StardewModdingAPI/Framework/ModHelpers/BaseHelper.cs rename to src/SMAPI/Framework/ModHelpers/BaseHelper.cs diff --git a/src/StardewModdingAPI/Framework/ModHelpers/CommandHelper.cs b/src/SMAPI/Framework/ModHelpers/CommandHelper.cs similarity index 100% rename from src/StardewModdingAPI/Framework/ModHelpers/CommandHelper.cs rename to src/SMAPI/Framework/ModHelpers/CommandHelper.cs diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs similarity index 100% rename from src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs rename to src/SMAPI/Framework/ModHelpers/ContentHelper.cs diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ModHelper.cs b/src/SMAPI/Framework/ModHelpers/ModHelper.cs similarity index 100% rename from src/StardewModdingAPI/Framework/ModHelpers/ModHelper.cs rename to src/SMAPI/Framework/ModHelpers/ModHelper.cs diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ModRegistryHelper.cs b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs similarity index 100% rename from src/StardewModdingAPI/Framework/ModHelpers/ModRegistryHelper.cs rename to src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ReflectionHelper.cs b/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs similarity index 100% rename from src/StardewModdingAPI/Framework/ModHelpers/ReflectionHelper.cs rename to src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs diff --git a/src/StardewModdingAPI/Framework/ModHelpers/TranslationHelper.cs b/src/SMAPI/Framework/ModHelpers/TranslationHelper.cs similarity index 100% rename from src/StardewModdingAPI/Framework/ModHelpers/TranslationHelper.cs rename to src/SMAPI/Framework/ModHelpers/TranslationHelper.cs diff --git a/src/StardewModdingAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs b/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs similarity index 100% rename from src/StardewModdingAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs rename to src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs diff --git a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoadStatus.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoadStatus.cs similarity index 100% rename from src/StardewModdingAPI/Framework/ModLoading/AssemblyLoadStatus.cs rename to src/SMAPI/Framework/ModLoading/AssemblyLoadStatus.cs diff --git a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs similarity index 100% rename from src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs rename to src/SMAPI/Framework/ModLoading/AssemblyLoader.cs diff --git a/src/StardewModdingAPI/Framework/ModLoading/AssemblyParseResult.cs b/src/SMAPI/Framework/ModLoading/AssemblyParseResult.cs similarity index 100% rename from src/StardewModdingAPI/Framework/ModLoading/AssemblyParseResult.cs rename to src/SMAPI/Framework/ModLoading/AssemblyParseResult.cs diff --git a/src/StardewModdingAPI/Framework/ModLoading/Finders/EventFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs similarity index 100% rename from src/StardewModdingAPI/Framework/ModLoading/Finders/EventFinder.cs rename to src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs diff --git a/src/StardewModdingAPI/Framework/ModLoading/Finders/FieldFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/FieldFinder.cs similarity index 100% rename from src/StardewModdingAPI/Framework/ModLoading/Finders/FieldFinder.cs rename to src/SMAPI/Framework/ModLoading/Finders/FieldFinder.cs diff --git a/src/StardewModdingAPI/Framework/ModLoading/Finders/MethodFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/MethodFinder.cs similarity index 100% rename from src/StardewModdingAPI/Framework/ModLoading/Finders/MethodFinder.cs rename to src/SMAPI/Framework/ModLoading/Finders/MethodFinder.cs diff --git a/src/StardewModdingAPI/Framework/ModLoading/Finders/PropertyFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/PropertyFinder.cs similarity index 100% rename from src/StardewModdingAPI/Framework/ModLoading/Finders/PropertyFinder.cs rename to src/SMAPI/Framework/ModLoading/Finders/PropertyFinder.cs diff --git a/src/StardewModdingAPI/Framework/ModLoading/Finders/TypeFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs similarity index 100% rename from src/StardewModdingAPI/Framework/ModLoading/Finders/TypeFinder.cs rename to src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs diff --git a/src/StardewModdingAPI/Framework/ModLoading/IInstructionHandler.cs b/src/SMAPI/Framework/ModLoading/IInstructionHandler.cs similarity index 100% rename from src/StardewModdingAPI/Framework/ModLoading/IInstructionHandler.cs rename to src/SMAPI/Framework/ModLoading/IInstructionHandler.cs diff --git a/src/StardewModdingAPI/Framework/ModLoading/IncompatibleInstructionException.cs b/src/SMAPI/Framework/ModLoading/IncompatibleInstructionException.cs similarity index 100% rename from src/StardewModdingAPI/Framework/ModLoading/IncompatibleInstructionException.cs rename to src/SMAPI/Framework/ModLoading/IncompatibleInstructionException.cs diff --git a/src/StardewModdingAPI/Framework/ModLoading/InstructionHandleResult.cs b/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs similarity index 100% rename from src/StardewModdingAPI/Framework/ModLoading/InstructionHandleResult.cs rename to src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs diff --git a/src/StardewModdingAPI/Framework/ModLoading/InvalidModStateException.cs b/src/SMAPI/Framework/ModLoading/InvalidModStateException.cs similarity index 100% rename from src/StardewModdingAPI/Framework/ModLoading/InvalidModStateException.cs rename to src/SMAPI/Framework/ModLoading/InvalidModStateException.cs diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModDependencyStatus.cs b/src/SMAPI/Framework/ModLoading/ModDependencyStatus.cs similarity index 100% rename from src/StardewModdingAPI/Framework/ModLoading/ModDependencyStatus.cs rename to src/SMAPI/Framework/ModLoading/ModDependencyStatus.cs diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs similarity index 100% rename from src/StardewModdingAPI/Framework/ModLoading/ModMetadata.cs rename to src/SMAPI/Framework/ModLoading/ModMetadata.cs diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModMetadataStatus.cs b/src/SMAPI/Framework/ModLoading/ModMetadataStatus.cs similarity index 100% rename from src/StardewModdingAPI/Framework/ModLoading/ModMetadataStatus.cs rename to src/SMAPI/Framework/ModLoading/ModMetadataStatus.cs diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs similarity index 100% rename from src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs rename to src/SMAPI/Framework/ModLoading/ModResolver.cs diff --git a/src/StardewModdingAPI/Framework/ModLoading/Platform.cs b/src/SMAPI/Framework/ModLoading/Platform.cs similarity index 100% rename from src/StardewModdingAPI/Framework/ModLoading/Platform.cs rename to src/SMAPI/Framework/ModLoading/Platform.cs diff --git a/src/StardewModdingAPI/Framework/ModLoading/PlatformAssemblyMap.cs b/src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs similarity index 100% rename from src/StardewModdingAPI/Framework/ModLoading/PlatformAssemblyMap.cs rename to src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs diff --git a/src/StardewModdingAPI/Framework/ModLoading/RewriteHelper.cs b/src/SMAPI/Framework/ModLoading/RewriteHelper.cs similarity index 100% rename from src/StardewModdingAPI/Framework/ModLoading/RewriteHelper.cs rename to src/SMAPI/Framework/ModLoading/RewriteHelper.cs diff --git a/src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs similarity index 100% rename from src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs rename to src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs diff --git a/src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs similarity index 100% rename from src/StardewModdingAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs rename to src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs diff --git a/src/StardewModdingAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs similarity index 100% rename from src/StardewModdingAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs rename to src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs diff --git a/src/StardewModdingAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs similarity index 100% rename from src/StardewModdingAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs rename to src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs diff --git a/src/StardewModdingAPI/Framework/ModLoading/Rewriters/VirtualEntryCallRemover.cs b/src/SMAPI/Framework/ModLoading/Rewriters/VirtualEntryCallRemover.cs similarity index 100% rename from src/StardewModdingAPI/Framework/ModLoading/Rewriters/VirtualEntryCallRemover.cs rename to src/SMAPI/Framework/ModLoading/Rewriters/VirtualEntryCallRemover.cs diff --git a/src/StardewModdingAPI/Framework/ModRegistry.cs b/src/SMAPI/Framework/ModRegistry.cs similarity index 100% rename from src/StardewModdingAPI/Framework/ModRegistry.cs rename to src/SMAPI/Framework/ModRegistry.cs diff --git a/src/StardewModdingAPI/Framework/Models/Manifest.cs b/src/SMAPI/Framework/Models/Manifest.cs similarity index 100% rename from src/StardewModdingAPI/Framework/Models/Manifest.cs rename to src/SMAPI/Framework/Models/Manifest.cs diff --git a/src/StardewModdingAPI/Framework/Models/ManifestDependency.cs b/src/SMAPI/Framework/Models/ManifestDependency.cs similarity index 100% rename from src/StardewModdingAPI/Framework/Models/ManifestDependency.cs rename to src/SMAPI/Framework/Models/ManifestDependency.cs diff --git a/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs b/src/SMAPI/Framework/Models/ModCompatibility.cs similarity index 100% rename from src/StardewModdingAPI/Framework/Models/ModCompatibility.cs rename to src/SMAPI/Framework/Models/ModCompatibility.cs diff --git a/src/StardewModdingAPI/Framework/Models/ModDataID.cs b/src/SMAPI/Framework/Models/ModDataID.cs similarity index 100% rename from src/StardewModdingAPI/Framework/Models/ModDataID.cs rename to src/SMAPI/Framework/Models/ModDataID.cs diff --git a/src/StardewModdingAPI/Framework/Models/ModDataRecord.cs b/src/SMAPI/Framework/Models/ModDataRecord.cs similarity index 100% rename from src/StardewModdingAPI/Framework/Models/ModDataRecord.cs rename to src/SMAPI/Framework/Models/ModDataRecord.cs diff --git a/src/StardewModdingAPI/Framework/Models/ModStatus.cs b/src/SMAPI/Framework/Models/ModStatus.cs similarity index 100% rename from src/StardewModdingAPI/Framework/Models/ModStatus.cs rename to src/SMAPI/Framework/Models/ModStatus.cs diff --git a/src/StardewModdingAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs similarity index 100% rename from src/StardewModdingAPI/Framework/Models/SConfig.cs rename to src/SMAPI/Framework/Models/SConfig.cs diff --git a/src/StardewModdingAPI/Framework/Monitor.cs b/src/SMAPI/Framework/Monitor.cs similarity index 100% rename from src/StardewModdingAPI/Framework/Monitor.cs rename to src/SMAPI/Framework/Monitor.cs diff --git a/src/StardewModdingAPI/Framework/Reflection/CacheEntry.cs b/src/SMAPI/Framework/Reflection/CacheEntry.cs similarity index 100% rename from src/StardewModdingAPI/Framework/Reflection/CacheEntry.cs rename to src/SMAPI/Framework/Reflection/CacheEntry.cs diff --git a/src/StardewModdingAPI/Framework/Reflection/PrivateField.cs b/src/SMAPI/Framework/Reflection/PrivateField.cs similarity index 100% rename from src/StardewModdingAPI/Framework/Reflection/PrivateField.cs rename to src/SMAPI/Framework/Reflection/PrivateField.cs diff --git a/src/StardewModdingAPI/Framework/Reflection/PrivateMethod.cs b/src/SMAPI/Framework/Reflection/PrivateMethod.cs similarity index 100% rename from src/StardewModdingAPI/Framework/Reflection/PrivateMethod.cs rename to src/SMAPI/Framework/Reflection/PrivateMethod.cs diff --git a/src/StardewModdingAPI/Framework/Reflection/PrivateProperty.cs b/src/SMAPI/Framework/Reflection/PrivateProperty.cs similarity index 100% rename from src/StardewModdingAPI/Framework/Reflection/PrivateProperty.cs rename to src/SMAPI/Framework/Reflection/PrivateProperty.cs diff --git a/src/StardewModdingAPI/Framework/Reflection/Reflector.cs b/src/SMAPI/Framework/Reflection/Reflector.cs similarity index 100% rename from src/StardewModdingAPI/Framework/Reflection/Reflector.cs rename to src/SMAPI/Framework/Reflection/Reflector.cs diff --git a/src/StardewModdingAPI/Framework/RequestExitDelegate.cs b/src/SMAPI/Framework/RequestExitDelegate.cs similarity index 100% rename from src/StardewModdingAPI/Framework/RequestExitDelegate.cs rename to src/SMAPI/Framework/RequestExitDelegate.cs diff --git a/src/StardewModdingAPI/Framework/SContentManager.cs b/src/SMAPI/Framework/SContentManager.cs similarity index 100% rename from src/StardewModdingAPI/Framework/SContentManager.cs rename to src/SMAPI/Framework/SContentManager.cs diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs similarity index 100% rename from src/StardewModdingAPI/Framework/SGame.cs rename to src/SMAPI/Framework/SGame.cs diff --git a/src/StardewModdingAPI/Framework/Serialisation/JsonHelper.cs b/src/SMAPI/Framework/Serialisation/JsonHelper.cs similarity index 100% rename from src/StardewModdingAPI/Framework/Serialisation/JsonHelper.cs rename to src/SMAPI/Framework/Serialisation/JsonHelper.cs diff --git a/src/StardewModdingAPI/Framework/Serialisation/SFieldConverter.cs b/src/SMAPI/Framework/Serialisation/SFieldConverter.cs similarity index 100% rename from src/StardewModdingAPI/Framework/Serialisation/SFieldConverter.cs rename to src/SMAPI/Framework/Serialisation/SFieldConverter.cs diff --git a/src/StardewModdingAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs b/src/SMAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs similarity index 100% rename from src/StardewModdingAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs rename to src/SMAPI/Framework/Serialisation/SelectiveStringEnumConverter.cs diff --git a/src/StardewModdingAPI/Framework/Utilities/ContextHash.cs b/src/SMAPI/Framework/Utilities/ContextHash.cs similarity index 100% rename from src/StardewModdingAPI/Framework/Utilities/ContextHash.cs rename to src/SMAPI/Framework/Utilities/ContextHash.cs diff --git a/src/StardewModdingAPI/Framework/Utilities/Countdown.cs b/src/SMAPI/Framework/Utilities/Countdown.cs similarity index 100% rename from src/StardewModdingAPI/Framework/Utilities/Countdown.cs rename to src/SMAPI/Framework/Utilities/Countdown.cs diff --git a/src/StardewModdingAPI/Framework/WebApiClient.cs b/src/SMAPI/Framework/WebApiClient.cs similarity index 100% rename from src/StardewModdingAPI/Framework/WebApiClient.cs rename to src/SMAPI/Framework/WebApiClient.cs diff --git a/src/StardewModdingAPI/IAssetData.cs b/src/SMAPI/IAssetData.cs similarity index 100% rename from src/StardewModdingAPI/IAssetData.cs rename to src/SMAPI/IAssetData.cs diff --git a/src/StardewModdingAPI/IAssetDataForDictionary.cs b/src/SMAPI/IAssetDataForDictionary.cs similarity index 100% rename from src/StardewModdingAPI/IAssetDataForDictionary.cs rename to src/SMAPI/IAssetDataForDictionary.cs diff --git a/src/StardewModdingAPI/IAssetDataForImage.cs b/src/SMAPI/IAssetDataForImage.cs similarity index 100% rename from src/StardewModdingAPI/IAssetDataForImage.cs rename to src/SMAPI/IAssetDataForImage.cs diff --git a/src/StardewModdingAPI/IAssetEditor.cs b/src/SMAPI/IAssetEditor.cs similarity index 100% rename from src/StardewModdingAPI/IAssetEditor.cs rename to src/SMAPI/IAssetEditor.cs diff --git a/src/StardewModdingAPI/IAssetInfo.cs b/src/SMAPI/IAssetInfo.cs similarity index 100% rename from src/StardewModdingAPI/IAssetInfo.cs rename to src/SMAPI/IAssetInfo.cs diff --git a/src/StardewModdingAPI/IAssetLoader.cs b/src/SMAPI/IAssetLoader.cs similarity index 100% rename from src/StardewModdingAPI/IAssetLoader.cs rename to src/SMAPI/IAssetLoader.cs diff --git a/src/StardewModdingAPI/ICommandHelper.cs b/src/SMAPI/ICommandHelper.cs similarity index 100% rename from src/StardewModdingAPI/ICommandHelper.cs rename to src/SMAPI/ICommandHelper.cs diff --git a/src/StardewModdingAPI/IContentHelper.cs b/src/SMAPI/IContentHelper.cs similarity index 100% rename from src/StardewModdingAPI/IContentHelper.cs rename to src/SMAPI/IContentHelper.cs diff --git a/src/StardewModdingAPI/ICursorPosition.cs b/src/SMAPI/ICursorPosition.cs similarity index 100% rename from src/StardewModdingAPI/ICursorPosition.cs rename to src/SMAPI/ICursorPosition.cs diff --git a/src/StardewModdingAPI/IManifest.cs b/src/SMAPI/IManifest.cs similarity index 100% rename from src/StardewModdingAPI/IManifest.cs rename to src/SMAPI/IManifest.cs diff --git a/src/StardewModdingAPI/IManifestDependency.cs b/src/SMAPI/IManifestDependency.cs similarity index 100% rename from src/StardewModdingAPI/IManifestDependency.cs rename to src/SMAPI/IManifestDependency.cs diff --git a/src/StardewModdingAPI/IMod.cs b/src/SMAPI/IMod.cs similarity index 100% rename from src/StardewModdingAPI/IMod.cs rename to src/SMAPI/IMod.cs diff --git a/src/StardewModdingAPI/IModHelper.cs b/src/SMAPI/IModHelper.cs similarity index 100% rename from src/StardewModdingAPI/IModHelper.cs rename to src/SMAPI/IModHelper.cs diff --git a/src/StardewModdingAPI/IModLinked.cs b/src/SMAPI/IModLinked.cs similarity index 100% rename from src/StardewModdingAPI/IModLinked.cs rename to src/SMAPI/IModLinked.cs diff --git a/src/StardewModdingAPI/IModRegistry.cs b/src/SMAPI/IModRegistry.cs similarity index 100% rename from src/StardewModdingAPI/IModRegistry.cs rename to src/SMAPI/IModRegistry.cs diff --git a/src/StardewModdingAPI/IMonitor.cs b/src/SMAPI/IMonitor.cs similarity index 100% rename from src/StardewModdingAPI/IMonitor.cs rename to src/SMAPI/IMonitor.cs diff --git a/src/StardewModdingAPI/IPrivateField.cs b/src/SMAPI/IPrivateField.cs similarity index 100% rename from src/StardewModdingAPI/IPrivateField.cs rename to src/SMAPI/IPrivateField.cs diff --git a/src/StardewModdingAPI/IPrivateMethod.cs b/src/SMAPI/IPrivateMethod.cs similarity index 100% rename from src/StardewModdingAPI/IPrivateMethod.cs rename to src/SMAPI/IPrivateMethod.cs diff --git a/src/StardewModdingAPI/IPrivateProperty.cs b/src/SMAPI/IPrivateProperty.cs similarity index 100% rename from src/StardewModdingAPI/IPrivateProperty.cs rename to src/SMAPI/IPrivateProperty.cs diff --git a/src/StardewModdingAPI/IReflectionHelper.cs b/src/SMAPI/IReflectionHelper.cs similarity index 100% rename from src/StardewModdingAPI/IReflectionHelper.cs rename to src/SMAPI/IReflectionHelper.cs diff --git a/src/StardewModdingAPI/ISemanticVersion.cs b/src/SMAPI/ISemanticVersion.cs similarity index 100% rename from src/StardewModdingAPI/ISemanticVersion.cs rename to src/SMAPI/ISemanticVersion.cs diff --git a/src/StardewModdingAPI/ITranslationHelper.cs b/src/SMAPI/ITranslationHelper.cs similarity index 100% rename from src/StardewModdingAPI/ITranslationHelper.cs rename to src/SMAPI/ITranslationHelper.cs diff --git a/src/StardewModdingAPI/LogLevel.cs b/src/SMAPI/LogLevel.cs similarity index 100% rename from src/StardewModdingAPI/LogLevel.cs rename to src/SMAPI/LogLevel.cs diff --git a/src/StardewModdingAPI/Metadata/CoreAssets.cs b/src/SMAPI/Metadata/CoreAssets.cs similarity index 100% rename from src/StardewModdingAPI/Metadata/CoreAssets.cs rename to src/SMAPI/Metadata/CoreAssets.cs diff --git a/src/StardewModdingAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs similarity index 100% rename from src/StardewModdingAPI/Metadata/InstructionMetadata.cs rename to src/SMAPI/Metadata/InstructionMetadata.cs diff --git a/src/StardewModdingAPI/Mod.cs b/src/SMAPI/Mod.cs similarity index 100% rename from src/StardewModdingAPI/Mod.cs rename to src/SMAPI/Mod.cs diff --git a/src/StardewModdingAPI/PatchMode.cs b/src/SMAPI/PatchMode.cs similarity index 100% rename from src/StardewModdingAPI/PatchMode.cs rename to src/SMAPI/PatchMode.cs diff --git a/src/StardewModdingAPI/Program.cs b/src/SMAPI/Program.cs similarity index 100% rename from src/StardewModdingAPI/Program.cs rename to src/SMAPI/Program.cs diff --git a/src/StardewModdingAPI/Properties/AssemblyInfo.cs b/src/SMAPI/Properties/AssemblyInfo.cs similarity index 100% rename from src/StardewModdingAPI/Properties/AssemblyInfo.cs rename to src/SMAPI/Properties/AssemblyInfo.cs diff --git a/src/StardewModdingAPI/SemanticVersion.cs b/src/SMAPI/SemanticVersion.cs similarity index 100% rename from src/StardewModdingAPI/SemanticVersion.cs rename to src/SMAPI/SemanticVersion.cs diff --git a/src/StardewModdingAPI/StardewModdingAPI.config.json b/src/SMAPI/StardewModdingAPI.config.json similarity index 100% rename from src/StardewModdingAPI/StardewModdingAPI.config.json rename to src/SMAPI/StardewModdingAPI.config.json diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj similarity index 97% rename from src/StardewModdingAPI/StardewModdingAPI.csproj rename to src/SMAPI/StardewModdingAPI.csproj index 3721a11b..c6ff75d1 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -1,4 +1,4 @@ - + @@ -87,7 +87,7 @@ - + Properties\GlobalAssemblyInfo.cs @@ -265,13 +265,13 @@ false - + - + {10db0676-9fc1-4771-a2c8-e2519f091e49} StardewModdingAPI.AssemblyRewriters - + \ No newline at end of file diff --git a/src/StardewModdingAPI/Translation.cs b/src/SMAPI/Translation.cs similarity index 100% rename from src/StardewModdingAPI/Translation.cs rename to src/SMAPI/Translation.cs diff --git a/src/StardewModdingAPI/Utilities/SButton.cs b/src/SMAPI/Utilities/SButton.cs similarity index 100% rename from src/StardewModdingAPI/Utilities/SButton.cs rename to src/SMAPI/Utilities/SButton.cs diff --git a/src/StardewModdingAPI/Utilities/SDate.cs b/src/SMAPI/Utilities/SDate.cs similarity index 100% rename from src/StardewModdingAPI/Utilities/SDate.cs rename to src/SMAPI/Utilities/SDate.cs diff --git a/src/StardewModdingAPI/icon.ico b/src/SMAPI/icon.ico similarity index 100% rename from src/StardewModdingAPI/icon.ico rename to src/SMAPI/icon.ico diff --git a/src/StardewModdingAPI/packages.config b/src/SMAPI/packages.config similarity index 100% rename from src/StardewModdingAPI/packages.config rename to src/SMAPI/packages.config diff --git a/src/StardewModdingAPI/steam_appid.txt b/src/SMAPI/steam_appid.txt similarity index 100% rename from src/StardewModdingAPI/steam_appid.txt rename to src/SMAPI/steam_appid.txt diff --git a/src/StardewModdingAPI/unix-launcher.sh b/src/SMAPI/unix-launcher.sh similarity index 100% rename from src/StardewModdingAPI/unix-launcher.sh rename to src/SMAPI/unix-launcher.sh diff --git a/src/TrainerMod/TrainerMod.csproj b/src/TrainerMod/TrainerMod.csproj index 383e8c28..3182338c 100644 --- a/src/TrainerMod/TrainerMod.csproj +++ b/src/TrainerMod/TrainerMod.csproj @@ -1,4 +1,4 @@ - + @@ -48,7 +48,7 @@ - + Properties\GlobalAssemblyInfo.cs @@ -85,7 +85,7 @@ - + {f1a573b0-f436-472c-ae29-0b91ea6b9f8f} StardewModdingAPI False @@ -98,5 +98,5 @@ - + \ No newline at end of file From a050dcede6a32213665922aefefbc1565ffdf06d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 7 Oct 2017 23:16:25 -0400 Subject: [PATCH 141/186] move technical docs out of README --- docs/README.md | 140 ++--------------------------------------- docs/technical-docs.md | 137 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 136 deletions(-) create mode 100644 docs/technical-docs.md diff --git a/docs/README.md b/docs/README.md index 01dd77ce..37aa50fe 100644 --- a/docs/README.md +++ b/docs/README.md @@ -40,140 +40,8 @@ doesn't change any of your game files. It serves five main purposes: crashing the game, and makes it possible to troubleshoot errors in the game itself that would otherwise show a generic 'program has stopped working' type of message._ -## For players -* [Intro & FAQs](http://stardewvalleywiki.com/Modding:Player_FAQs) -* [Installing SMAPI](http://stardewvalleywiki.com/Modding:Installing_SMAPI) -* [Release notes](release-notes.md#release-notes) -* Need help? Come [chat on Discord](https://discord.gg/KCJHWhX) or [post in the support forums](http://community.playstarbound.com/threads/smapi-stardew-modding-api.108375/). - _Please don't submit issues on GitHub for support questions._ - -## For mod developers -* [Modding documentation](http://stardewvalleywiki.com/Modding:Index) -* [Release notes](release-notes.md#release-notes) +## Documentation +* [For players & mod creators](http://stardewvalleywiki.com/Modding:Index) +* [Release notes](release-notes.md) +* [Technical docs](technical-docs.md) * [Chat on Discord](https://discord.gg/KCJHWhX) with SMAPI developers and other modders - -## For SMAPI developers -_This section is about compiling SMAPI itself from source. If you don't know what that means, this -section isn't relevant to you; see the previous sections to use or create mods._ - -### Compiling from source -Using an official SMAPI release is recommended for most users. - -SMAPI uses some C# 7 code, so you'll need at least -[Visual Studio 2017](https://www.visualstudio.com/vs/community/) on Windows, -[MonoDevelop 7.0](http://www.monodevelop.com/) on Linux, -[Visual Studio 2017 for Mac](https://www.visualstudio.com/vs/visual-studio-mac/), or an equivalent -IDE to compile it. It uses build configuration derived from the -[crossplatform mod config](https://github.com/Pathoschild/Stardew.ModBuildConfig#readme) to detect -your current OS automatically and load the correct references. Compile output will be placed in a -`bin` folder at the root of the git repository. - -### Debugging a local build -Rebuilding the solution in debug mode will copy the SMAPI files into your game folder. Starting -the `StardewModdingAPI` project with debugging from Visual Studio (on Mac or Windows) will launch -SMAPI with the debugger attached, so you can intercept errors and step through the code being -executed. This doesn't work in MonoDevelop on Linux, unfortunately. - -### Preparing a release -To prepare a crossplatform SMAPI release, you'll need to compile it on two platforms. See -[crossplatforming info](http://stardewvalleywiki.com/Modding:Creating_a_SMAPI_mod#Test_on_all_platforms) -on the wiki for the first-time setup. - -1. Update the version number in `GlobalAssemblyInfo.cs` and `Constants::Version`. Make sure you use a - [semantic version](http://semver.org). Recommended format: - - build type | format | example - :--------- | :-------------------------------- | :------ - dev build | `-alpha.` | `2.0-alpha.20171230` - prerelease | `-prerelease.` | `2.0-prerelease.2` - release | `` | `2.0` - -2. In Windows: - 1. Rebuild the solution in _Release_ mode. - 2. Rename `bin/Packaged` to `SMAPI ` (e.g. `SMAPI 2.0`). - 2. Transfer the `SMAPI ` folder to Linux or Mac. - _This adds the installer executable and Windows files. We'll do the rest in Linux or Mac, - since we need to set Unix file permissions that Windows won't save._ - -2. In Linux or Mac: - 1. Rebuild the solution in _Release_ mode. - 2. Copy `bin/internal/Packaged/Mono` into the `SMAPI ` folder. - 3. If you did everything right so far, you should have a folder like this: - - ``` - SMAPI-2.x/ - install.exe - readme.txt - internal/ - Mono/ - Mods/* - Mono.Cecil.dll - Newtonsoft.Json.dll - StardewModdingAPI - StardewModdingAPI.AssemblyRewriters.dll - StardewModdingAPI.config.json - StardewModdingAPI.exe - StardewModdingAPI.pdb - StardewModdingAPI.xml - steam_appid.txt - System.Numerics.dll - System.Runtime.Caching.dll - System.ValueTuple.dll - Windows/ - Mods/* - Mono.Cecil.dll - Newtonsoft.Json.dll - StardewModdingAPI.AssemblyRewriters.dll - StardewModdingAPI.config.json - StardewModdingAPI.exe - StardewModdingAPI.pdb - StardewModdingAPI.xml - System.ValueTuple.dll - steam_appid.txt - ``` - 4. Open a terminal in the `SMAPI ` folder and run `chmod 755 internal/Mono/StardewModdingAPI`. - 5. Copy & paste the `SMAPI ` folder as `SMAPI for developers`. - 6. In the `SMAPI ` folder... - * edit `internal/Mono/StardewModdingAPI.config.json` and - `internal/Windows/StardewModdingAPI.config.json` to disable developer mode; - * delete `internal/Windows/StardewModdingAPI.xml`. - 7. Compress the two folders into `SMAPI .zip` and `SMAPI for developers.zip`. - -## Advanced usage -### Configuration file -You can customise the SMAPI behaviour by editing the `StardewModdingAPI.config.json` file in your -game folder. - -Basic fields: - -field | purpose ------------------ | ------- -`DeveloperMode` | Default `false` (except in _SMAPI for developers_ releases). Whether to enable features intended for mod developers (mainly more detailed console logging). -`CheckForUpdates` | Default `true`. Whether SMAPI should check for a newer version when you load the game. If a new version is available, a small message will appear in the console. This doesn't affect the load time even if your connection is offline or slow, because it happens in the background. -`VerboseLogging` | Default `false`. Whether SMAPI should log more information about the game context. -`ModData` | Internal metadata about SMAPI mods. Changing this isn't recommended and may destabilise your game. See documentation in the file. - -### Command-line arguments -The SMAPI installer recognises three command-line arguments: - -argument | purpose --------- | ------- -`--install` | Preselects the install action, skipping the prompt asking what the user wants to do. -`--uninstall` | Preselects the uninstall action, skipping the prompt asking what the user wants to do. -`--game-path "path"` | Specifies the full path to the folder containing the Stardew Valley executable, skipping automatic detection and any prompt to choose a path. If the path is not valid, the installer displays an error. - -SMAPI itself recognises two arguments, but these are intended for internal use or testing and may -change without warning. - -argument | purpose --------- | ------- -`--log-path "path"` | The relative or absolute path of the log file SMAPI should write. -`--no-terminal` | SMAPI won't write anything to the console window. (Messages will still be written to the log file.) - -### Compile flags -SMAPI uses a small number of conditional compilation constants, which you can set by editing the -`` element in `StardewModdingAPI.csproj`. Supported constants: - -flag | purpose ----- | ------- -`SMAPI_FOR_WINDOWS` | Indicates that SMAPI is being compiled on Windows for players on Windows. Set automatically in `crossplatform.targets`. diff --git a/docs/technical-docs.md b/docs/technical-docs.md new file mode 100644 index 00000000..d37d327d --- /dev/null +++ b/docs/technical-docs.md @@ -0,0 +1,137 @@ +← [README](README.md) + +This file provides more technical documentation about SMAPI. If you only want to use or create +mods, this section isn't relevant to you; see the main README to use or create mods. + +## Contents +* [Development](#development) + * [Compiling from source](#compiling-from-source) + * [Debugging a local build](#debugging-a-local-build) + * [Preparing a release](#preparing-a-release) +* [Customisation](#customisation) + * [Configuration file](#configuration-file) + * [Command-line arguments](#command-line-arguments) + * [Compile flags](#compile-flags) + +## Development +### Compiling from source +Using an official SMAPI release is recommended for most users. + +SMAPI uses some C# 7 code, so you'll need at least +[Visual Studio 2017](https://www.visualstudio.com/vs/community/) on Windows, +[MonoDevelop 7.0](http://www.monodevelop.com/) on Linux, +[Visual Studio 2017 for Mac](https://www.visualstudio.com/vs/visual-studio-mac/), or an equivalent +IDE to compile it. It uses build configuration derived from the +[crossplatform mod config](https://github.com/Pathoschild/Stardew.ModBuildConfig#readme) to detect +your current OS automatically and load the correct references. Compile output will be placed in a +`bin` folder at the root of the git repository. + +### Debugging a local build +Rebuilding the solution in debug mode will copy the SMAPI files into your game folder. Starting +the `StardewModdingAPI` project with debugging from Visual Studio (on Mac or Windows) will launch +SMAPI with the debugger attached, so you can intercept errors and step through the code being +executed. This doesn't work in MonoDevelop on Linux, unfortunately. + +### Preparing a release +To prepare a crossplatform SMAPI release, you'll need to compile it on two platforms. See +[crossplatforming info](http://stardewvalleywiki.com/Modding:Creating_a_SMAPI_mod#Test_on_all_platforms) +on the wiki for the first-time setup. + +1. Update the version number in `GlobalAssemblyInfo.cs` and `Constants::Version`. Make sure you use a + [semantic version](http://semver.org). Recommended format: + + build type | format | example + :--------- | :-------------------------------- | :------ + dev build | `-alpha.` | `2.0-alpha.20171230` + prerelease | `-prerelease.` | `2.0-prerelease.2` + release | `` | `2.0` + +2. In Windows: + 1. Rebuild the solution in _Release_ mode. + 2. Rename `bin/Packaged` to `SMAPI ` (e.g. `SMAPI 2.0`). + 2. Transfer the `SMAPI ` folder to Linux or Mac. + _This adds the installer executable and Windows files. We'll do the rest in Linux or Mac, + since we need to set Unix file permissions that Windows won't save._ + +2. In Linux or Mac: + 1. Rebuild the solution in _Release_ mode. + 2. Copy `bin/internal/Packaged/Mono` into the `SMAPI ` folder. + 3. If you did everything right so far, you should have a folder like this: + + ``` + SMAPI-2.x/ + install.exe + readme.txt + internal/ + Mono/ + Mods/* + Mono.Cecil.dll + Newtonsoft.Json.dll + StardewModdingAPI + StardewModdingAPI.AssemblyRewriters.dll + StardewModdingAPI.config.json + StardewModdingAPI.exe + StardewModdingAPI.pdb + StardewModdingAPI.xml + steam_appid.txt + System.Numerics.dll + System.Runtime.Caching.dll + System.ValueTuple.dll + Windows/ + Mods/* + Mono.Cecil.dll + Newtonsoft.Json.dll + StardewModdingAPI.AssemblyRewriters.dll + StardewModdingAPI.config.json + StardewModdingAPI.exe + StardewModdingAPI.pdb + StardewModdingAPI.xml + System.ValueTuple.dll + steam_appid.txt + ``` + 4. Open a terminal in the `SMAPI ` folder and run `chmod 755 internal/Mono/StardewModdingAPI`. + 5. Copy & paste the `SMAPI ` folder as `SMAPI for developers`. + 6. In the `SMAPI ` folder... + * edit `internal/Mono/StardewModdingAPI.config.json` and + `internal/Windows/StardewModdingAPI.config.json` to disable developer mode; + * delete `internal/Windows/StardewModdingAPI.xml`. + 7. Compress the two folders into `SMAPI .zip` and `SMAPI for developers.zip`. + +## Customisation +### Configuration file +You can customise the SMAPI behaviour by editing the `StardewModdingAPI.config.json` file in your +game folder. + +Basic fields: + +field | purpose +----------------- | ------- +`DeveloperMode` | Default `false` (except in _SMAPI for developers_ releases). Whether to enable features intended for mod developers (mainly more detailed console logging). +`CheckForUpdates` | Default `true`. Whether SMAPI should check for a newer version when you load the game. If a new version is available, a small message will appear in the console. This doesn't affect the load time even if your connection is offline or slow, because it happens in the background. +`VerboseLogging` | Default `false`. Whether SMAPI should log more information about the game context. +`ModData` | Internal metadata about SMAPI mods. Changing this isn't recommended and may destabilise your game. See documentation in the file. + +### Command-line arguments +The SMAPI installer recognises three command-line arguments: + +argument | purpose +-------- | ------- +`--install` | Preselects the install action, skipping the prompt asking what the user wants to do. +`--uninstall` | Preselects the uninstall action, skipping the prompt asking what the user wants to do. +`--game-path "path"` | Specifies the full path to the folder containing the Stardew Valley executable, skipping automatic detection and any prompt to choose a path. If the path is not valid, the installer displays an error. + +SMAPI itself recognises two arguments, but these are intended for internal use or testing and may +change without warning. + +argument | purpose +-------- | ------- +`--log-path "path"` | The relative or absolute path of the log file SMAPI should write. +`--no-terminal` | SMAPI won't write anything to the console window. (Messages will still be written to the log file.) + +### Compile flags +SMAPI uses a small number of conditional compilation constants, which you can set by editing the +`` element in `StardewModdingAPI.csproj`. Supported constants: + +flag | purpose +---- | ------- +`SMAPI_FOR_WINDOWS` | Indicates that SMAPI is being compiled on Windows for players on Windows. Set automatically in `crossplatform.targets`. From a0fc392002b98ffe803d48be70cd272e22706194 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 7 Oct 2017 23:18:06 -0400 Subject: [PATCH 142/186] update readme --- docs/README.md | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/docs/README.md b/docs/README.md index 37aa50fe..2adf2bf0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,21 +1,9 @@ ![](docs/imgs/SMAPI.png) -## Contents -* [What is SMAPI?](#what-is-smapi) -* **[For players](#for-players)** -* **[For mod developers](#for-mod-developers)** -* [For SMAPI developers](#for-smapi-developers) - * [Compiling from source](#compiling-from-source) - * [Debugging a local build](#debugging-a-local-build) - * [Preparing a release](#preparing-a-release) -* [Advanced usage](#advanced-usage) - * [Configuration file](#configuration-file) - * [Command-line arguments](#command-line-arguments) - ## What is SMAPI? -**SMAPI** is an [open-source](LICENSE) modding API for [Stardew Valley](http://stardewvalley.net/) +**SMAPI** is an [open-source](LICENSE.md) modding API for [Stardew Valley](http://stardewvalley.net/) that lets you play the game with mods. It's safely installed alongside the game's executable, and -doesn't change any of your game files. It serves five main purposes: +doesn't change any of your game files. It serves six main purposes: 1. **Load mods into the game.** _SMAPI loads mods when the game is starting up so they can interact with it. (Code mods aren't @@ -40,6 +28,10 @@ doesn't change any of your game files. It serves five main purposes: crashing the game, and makes it possible to troubleshoot errors in the game itself that would otherwise show a generic 'program has stopped working' type of message._ +6. **Provide update checks.** + _SMAPI automatically checks for new versions of your installed mods, and notifies you when any + are available._ + ## Documentation * [For players & mod creators](http://stardewvalleywiki.com/Modding:Index) * [Release notes](release-notes.md) From 5928f5f86c4493ddb6b89bce0b7d0fb73a884c09 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 7 Oct 2017 23:19:23 -0400 Subject: [PATCH 143/186] remove readme header image --- docs/README.md | 3 --- docs/imgs/SMAPI.png | Bin 252371 -> 0 bytes 2 files changed, 3 deletions(-) delete mode 100644 docs/imgs/SMAPI.png diff --git a/docs/README.md b/docs/README.md index 2adf2bf0..92a39dbe 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,3 @@ -![](docs/imgs/SMAPI.png) - -## What is SMAPI? **SMAPI** is an [open-source](LICENSE.md) modding API for [Stardew Valley](http://stardewvalley.net/) that lets you play the game with mods. It's safely installed alongside the game's executable, and doesn't change any of your game files. It serves six main purposes: diff --git a/docs/imgs/SMAPI.png b/docs/imgs/SMAPI.png deleted file mode 100644 index 50f375b69b83d6f74f1f866348f5860e252e70a1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 252371 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

;URPsez7&S<2uR5UoVlX?<*@1cusLNsH*W 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�pv#YZ**~geuUN%>Y4(a81J4YD8jX@z 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 XzRenCCLhLL;wE)bnnP; From 99617211fa1d17aa11016b5dd5c5cb63d3b1ac16 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 7 Oct 2017 23:33:03 -0400 Subject: [PATCH 144/186] move license back to root so GitHub sees it, readme tweaks --- docs/LICENSE.md => LICENSE.txt | 0 docs/README.md | 14 +++++++------- src/SMAPI.sln | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) rename docs/LICENSE.md => LICENSE.txt (100%) diff --git a/docs/LICENSE.md b/LICENSE.txt similarity index 100% rename from docs/LICENSE.md rename to LICENSE.txt diff --git a/docs/README.md b/docs/README.md index 92a39dbe..e2d39211 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,14 +1,14 @@ -**SMAPI** is an [open-source](LICENSE.md) modding API for [Stardew Valley](http://stardewvalley.net/) -that lets you play the game with mods. It's safely installed alongside the game's executable, and -doesn't change any of your game files. It serves six main purposes: +**SMAPI** is an open-source modding API for [Stardew Valley](http://stardewvalley.net/) that lets +you play the game with mods. It's safely installed alongside the game's executable, and doesn't +change any of your game files. It serves six main purposes: 1. **Load mods into the game.** _SMAPI loads mods when the game is starting up so they can interact with it. (Code mods aren't possible without SMAPI to load them.)_ 2. **Provide APIs and events for mods.** - _SMAPI provides low-level APIs and events which let mods interact with the game in ways they - otherwise couldn't._ + _SMAPI provides APIs and events which let mods interact with the game in ways they otherwise + couldn't._ 3. **Rewrite mods for crossplatform compatibility.** _SMAPI rewrites mods' compiled code before loading them so they work on Linux/Mac/Windows @@ -16,8 +16,8 @@ doesn't change any of your game files. It serves six main purposes: game._ 4. **Rewrite mods to update them.** - _SMAPI detects when a mod accesses part of the game that changed in a recent update which - affects many mods, and rewrites the mod so it's compatible._ + _SMAPI detects when a mod accesses part of the game that changed in a game update which affects + many mods, and rewrites the mod so it's compatible._ 5. **Intercept errors.** _SMAPI intercepts errors that happen in the game, displays the error details in the console diff --git a/src/SMAPI.sln b/src/SMAPI.sln index 5936ff43..fcb805a7 100644 --- a/src/SMAPI.sln +++ b/src/SMAPI.sln @@ -12,6 +12,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".root", ".root", "{86C452BE ..\.editorconfig = ..\.editorconfig ..\.gitattributes = ..\.gitattributes ..\.gitignore = ..\.gitignore + ..\LICENSE.txt = ..\LICENSE.txt EndProjectSection EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Installer", "SMAPI.Installer\StardewModdingAPI.Installer.csproj", "{443DDF81-6AAF-420A-A610-3459F37E5575}" @@ -33,7 +34,6 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{EB35A917-67B9-4EFA-8DFC-4FB49B3949BB}" ProjectSection(SolutionItems) = preProject ..\docs\CONTRIBUTING.md = ..\docs\CONTRIBUTING.md - ..\docs\LICENSE.md = ..\docs\LICENSE.md ..\docs\README.md = ..\docs\README.md ..\docs\release-notes.md = ..\docs\release-notes.md EndProjectSection From b7fb188513a844afed66b9b292ecfddb58528a42 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 7 Oct 2017 23:57:47 -0400 Subject: [PATCH 145/186] rename shared project for broader use --- .../Models}/ModInfoModel.cs | 2 +- .../Models}/ModSeachModel.cs | 2 +- .../StardewModdingAPI.Common.projitems} | 9 ++++++--- .../StardewModdingAPI.Common.shproj} | 2 +- src/SMAPI.Web/Controllers/ModsController.cs | 2 +- .../Framework/ModRepositories/BaseRepository.cs | 2 +- .../Framework/ModRepositories/ChucklefishRepository.cs | 2 +- .../Framework/ModRepositories/GitHubRepository.cs | 2 +- .../Framework/ModRepositories/IModRepository.cs | 2 +- .../Framework/ModRepositories/NexusRepository.cs | 2 +- src/SMAPI.Web/StardewModdingAPI.Web.csproj | 2 +- src/SMAPI.sln | 6 +++--- src/SMAPI/Framework/WebApiClient.cs | 2 +- src/SMAPI/Program.cs | 2 +- src/SMAPI/StardewModdingAPI.csproj | 2 +- 15 files changed, 22 insertions(+), 19 deletions(-) rename src/{SMAPI.Models => SMAPI.Common/Models}/ModInfoModel.cs (97%) rename src/{SMAPI.Models => SMAPI.Common/Models}/ModSeachModel.cs (95%) rename src/{SMAPI.Models/StardewModdingAPI.Models.projitems => SMAPI.Common/StardewModdingAPI.Common.projitems} (58%) rename src/{SMAPI.Models/StardewModdingAPI.Models.shproj => SMAPI.Common/StardewModdingAPI.Common.shproj} (93%) diff --git a/src/SMAPI.Models/ModInfoModel.cs b/src/SMAPI.Common/Models/ModInfoModel.cs similarity index 97% rename from src/SMAPI.Models/ModInfoModel.cs rename to src/SMAPI.Common/Models/ModInfoModel.cs index 44071230..e071c0bb 100644 --- a/src/SMAPI.Models/ModInfoModel.cs +++ b/src/SMAPI.Common/Models/ModInfoModel.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace StardewModdingAPI.Models +namespace StardewModdingAPI.Common.Models { ///

Generic metadata about a mod. internal class ModInfoModel diff --git a/src/SMAPI.Models/ModSeachModel.cs b/src/SMAPI.Common/Models/ModSeachModel.cs similarity index 95% rename from src/SMAPI.Models/ModSeachModel.cs rename to src/SMAPI.Common/Models/ModSeachModel.cs index 526fbaf3..3f69f0ae 100644 --- a/src/SMAPI.Models/ModSeachModel.cs +++ b/src/SMAPI.Common/Models/ModSeachModel.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Linq; -namespace StardewModdingAPI.Models +namespace StardewModdingAPI.Common.Models { /// Specifies mods whose update-check info to fetch. internal class ModSearchModel diff --git a/src/SMAPI.Models/StardewModdingAPI.Models.projitems b/src/SMAPI.Common/StardewModdingAPI.Common.projitems similarity index 58% rename from src/SMAPI.Models/StardewModdingAPI.Models.projitems rename to src/SMAPI.Common/StardewModdingAPI.Common.projitems index e2cb29e1..b3296570 100644 --- a/src/SMAPI.Models/StardewModdingAPI.Models.projitems +++ b/src/SMAPI.Common/StardewModdingAPI.Common.projitems @@ -6,10 +6,13 @@ 2aa02fb6-ff03-41cf-a215-2ee60ab4f5dc - StardewModdingAPI.Models + StardewModdingAPI.Common - - + + + + + \ No newline at end of file diff --git a/src/SMAPI.Models/StardewModdingAPI.Models.shproj b/src/SMAPI.Common/StardewModdingAPI.Common.shproj similarity index 93% rename from src/SMAPI.Models/StardewModdingAPI.Models.shproj rename to src/SMAPI.Common/StardewModdingAPI.Common.shproj index c80517af..0ef29144 100644 --- a/src/SMAPI.Models/StardewModdingAPI.Models.shproj +++ b/src/SMAPI.Common/StardewModdingAPI.Common.shproj @@ -8,6 +8,6 @@ - + diff --git a/src/SMAPI.Web/Controllers/ModsController.cs b/src/SMAPI.Web/Controllers/ModsController.cs index 7dcfcf13..a671ddca 100644 --- a/src/SMAPI.Web/Controllers/ModsController.cs +++ b/src/SMAPI.Web/Controllers/ModsController.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; -using StardewModdingAPI.Models; +using StardewModdingAPI.Common.Models; using StardewModdingAPI.Web.Framework.ConfigModels; using StardewModdingAPI.Web.Framework.ModRepositories; diff --git a/src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs index d98acd89..edb00454 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs @@ -1,6 +1,6 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; -using StardewModdingAPI.Models; +using StardewModdingAPI.Common.Models; namespace StardewModdingAPI.Web.Framework.ModRepositories { diff --git a/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs index ed7bd60b..06ec58ed 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs @@ -3,7 +3,7 @@ using System.Net; using System.Threading.Tasks; using HtmlAgilityPack; using Pathoschild.Http.Client; -using StardewModdingAPI.Models; +using StardewModdingAPI.Common.Models; namespace StardewModdingAPI.Web.Framework.ModRepositories { diff --git a/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs index 174fb79a..9d43adf0 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs @@ -3,7 +3,7 @@ using System.Net; using System.Threading.Tasks; using Newtonsoft.Json; using Pathoschild.Http.Client; -using StardewModdingAPI.Models; +using StardewModdingAPI.Common.Models; namespace StardewModdingAPI.Web.Framework.ModRepositories { diff --git a/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs index 98e4c957..4496400c 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs @@ -1,6 +1,6 @@ using System; using System.Threading.Tasks; -using StardewModdingAPI.Models; +using StardewModdingAPI.Common.Models; namespace StardewModdingAPI.Web.Framework.ModRepositories { diff --git a/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs index 71970bec..8a4bb0d8 100644 --- a/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs +++ b/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs @@ -2,7 +2,7 @@ using System; using System.Threading.Tasks; using Newtonsoft.Json; using Pathoschild.Http.Client; -using StardewModdingAPI.Models; +using StardewModdingAPI.Common.Models; namespace StardewModdingAPI.Web.Framework.ModRepositories { diff --git a/src/SMAPI.Web/StardewModdingAPI.Web.csproj b/src/SMAPI.Web/StardewModdingAPI.Web.csproj index 6b1d0687..ec1311f4 100644 --- a/src/SMAPI.Web/StardewModdingAPI.Web.csproj +++ b/src/SMAPI.Web/StardewModdingAPI.Web.csproj @@ -21,6 +21,6 @@ - + diff --git a/src/SMAPI.sln b/src/SMAPI.sln index fcb805a7..7941394d 100644 --- a/src/SMAPI.sln +++ b/src/SMAPI.sln @@ -29,7 +29,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StardewModdingAPI.Web", "SM EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Internal", "Internal", "{82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11}" EndProject -Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "StardewModdingAPI.Models", "SMAPI.Models\StardewModdingAPI.Models.shproj", "{2AA02FB6-FF03-41CF-A215-2EE60AB4F5DC}" +Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "StardewModdingAPI.Common", "SMAPI.Common\StardewModdingAPI.Common.shproj", "{2AA02FB6-FF03-41CF-A215-2EE60AB4F5DC}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{EB35A917-67B9-4EFA-8DFC-4FB49B3949BB}" ProjectSection(SolutionItems) = preProject @@ -47,8 +47,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{09CF91E5 EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution - SMAPI.Models\StardewModdingAPI.Models.projitems*{2aa02fb6-ff03-41cf-a215-2ee60ab4f5dc}*SharedItemsImports = 13 - SMAPI.Models\StardewModdingAPI.Models.projitems*{f1a573b0-f436-472c-ae29-0b91ea6b9f8f}*SharedItemsImports = 4 + SMAPI.Common\StardewModdingAPI.Common.projitems*{2aa02fb6-ff03-41cf-a215-2ee60ab4f5dc}*SharedItemsImports = 13 + SMAPI.Common\StardewModdingAPI.Common.projitems*{f1a573b0-f436-472c-ae29-0b91ea6b9f8f}*SharedItemsImports = 4 EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/src/SMAPI/Framework/WebApiClient.cs b/src/SMAPI/Framework/WebApiClient.cs index f3c7de28..e78ac14b 100644 --- a/src/SMAPI/Framework/WebApiClient.cs +++ b/src/SMAPI/Framework/WebApiClient.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.Net; using Newtonsoft.Json; -using StardewModdingAPI.Models; +using StardewModdingAPI.Common.Models; namespace StardewModdingAPI.Framework { diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 7dfdc745..293c9da7 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -12,6 +12,7 @@ using System.Management; using System.Windows.Forms; #endif using Newtonsoft.Json; +using StardewModdingAPI.Common.Models; using StardewModdingAPI.Events; using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.Exceptions; @@ -21,7 +22,6 @@ using StardewModdingAPI.Framework.ModHelpers; using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Serialisation; -using StardewModdingAPI.Models; using StardewValley; using Monitor = StardewModdingAPI.Framework.Monitor; using SObject = StardewValley.Object; diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index c6ff75d1..35a784cd 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -265,7 +265,7 @@ false - + {10db0676-9fc1-4771-a2c8-e2519f091e49} From 51f5be1e740f8897339afbbd5916fd627c081db0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 8 Oct 2017 00:09:16 -0400 Subject: [PATCH 146/186] move semver implementation into shared project for reuse --- src/SMAPI.Common/SemanticVersionImpl.cs | 192 ++++++++++++++++++ .../StardewModdingAPI.Common.projitems | 1 + src/SMAPI/SemanticVersion.cs | 129 ++---------- 3 files changed, 214 insertions(+), 108 deletions(-) create mode 100644 src/SMAPI.Common/SemanticVersionImpl.cs diff --git a/src/SMAPI.Common/SemanticVersionImpl.cs b/src/SMAPI.Common/SemanticVersionImpl.cs new file mode 100644 index 00000000..c257aaaf --- /dev/null +++ b/src/SMAPI.Common/SemanticVersionImpl.cs @@ -0,0 +1,192 @@ +using System; +using System.Text.RegularExpressions; +using Newtonsoft.Json; + +namespace StardewModdingAPI.Common +{ + /// A low-level implementation of a semantic version with an optional release tag. + /// The implementation is defined by Semantic Version 2.0 (http://semver.org/). + internal class SemanticVersionImpl + { + /********* + ** Properties + *********/ + /// A regular expression matching a semantic version string. + /// + /// This pattern is derived from the BNF documentation in the semver repo, + /// with three important deviations intended to support Stardew Valley mod conventions: + /// - allows short-form "x.y" versions; + /// - allows hyphens in prerelease tags as synonyms for dots (like "-unofficial-update.3"); + /// - doesn't allow '+build' suffixes. + /// + private static readonly Regex Regex = new Regex(@"^(?>(?0|[1-9]\d*))\.(?>(?0|[1-9]\d*))(?>(?:\.(?0|[1-9]\d*))?)(?:-(?(?>[a-z0-9]+[\-\.]?)+))?$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.ExplicitCapture); + + + /********* + ** Accessors + *********/ + /// The major version incremented for major API changes. + public int Major { get; } + + /// The minor version incremented for backwards-compatible changes. + public int Minor { get; } + + /// The patch version for backwards-compatible bug fixes. + public int Patch { get; } + + /// An optional prerelease tag. + public string Tag { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The major version incremented for major API changes. + /// The minor version incremented for backwards-compatible changes. + /// The patch version for backwards-compatible bug fixes. + /// An optional prerelease tag. + [JsonConstructor] + public SemanticVersionImpl(int major, int minor, int patch, string tag = null) + { + this.Major = major; + this.Minor = minor; + this.Patch = patch; + this.Tag = this.GetNormalisedTag(tag); + } + + /// Construct an instance. + /// The semantic version string. + /// The is null. + /// The is not a valid semantic version. + public SemanticVersionImpl(string version) + { + // parse + if (version == null) + throw new ArgumentNullException(nameof(version), "The input version string can't be null."); + var match = SemanticVersionImpl.Regex.Match(version.Trim()); + if (!match.Success) + throw new FormatException($"The input '{version}' isn't a valid semantic version."); + + // initialise + this.Major = int.Parse(match.Groups["major"].Value); + this.Minor = match.Groups["minor"].Success ? int.Parse(match.Groups["minor"].Value) : 0; + this.Patch = match.Groups["patch"].Success ? int.Parse(match.Groups["patch"].Value) : 0; + this.Tag = match.Groups["prerelease"].Success ? this.GetNormalisedTag(match.Groups["prerelease"].Value) : null; + } + + /// Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version. + /// The version to compare with this instance. + /// The value is null. + public int CompareTo(SemanticVersionImpl other) + { + if (other == null) + throw new ArgumentNullException(nameof(other)); + return this.CompareTo(other.Major, other.Minor, other.Patch, other.Tag); + } + + + /// Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version. + /// The major version to compare with this instance. + /// The minor version to compare with this instance. + /// The patch version to compare with this instance. + /// The prerelease tag to compare with this instance. + public int CompareTo(int otherMajor, int otherMinor, int otherPatch, string otherTag) + { + const int same = 0; + const int curNewer = 1; + const int curOlder = -1; + + // compare stable versions + if (this.Major != otherMajor) + return this.Major.CompareTo(otherMajor); + if (this.Minor != otherMinor) + return this.Minor.CompareTo(otherMinor); + if (this.Patch != otherPatch) + return this.Patch.CompareTo(otherPatch); + if (this.Tag == otherTag) + return same; + + // stable supercedes pre-release + bool curIsStable = string.IsNullOrWhiteSpace(this.Tag); + bool otherIsStable = string.IsNullOrWhiteSpace(otherTag); + if (curIsStable) + return curNewer; + if (otherIsStable) + return curOlder; + + // compare two pre-release tag values + string[] curParts = this.Tag.Split('.', '-'); + string[] otherParts = otherTag.Split('.', '-'); + for (int i = 0; i < curParts.Length; i++) + { + // longer prerelease tag supercedes if otherwise equal + if (otherParts.Length <= i) + return curNewer; + + // compare if different + if (curParts[i] != otherParts[i]) + { + // compare numerically if possible + { + if (int.TryParse(curParts[i], out int curNum) && int.TryParse(otherParts[i], out int otherNum)) + return curNum.CompareTo(otherNum); + } + + // else compare lexically + return string.Compare(curParts[i], otherParts[i], StringComparison.OrdinalIgnoreCase); + } + } + + // fallback (this should never happen) + return string.Compare(this.ToString(), new SemanticVersionImpl(otherMajor, otherMinor, otherPatch, otherTag).ToString(), StringComparison.InvariantCultureIgnoreCase); + } + + /// Get a string representation of the version. + public override string ToString() + { + // version + string result = this.Patch != 0 + ? $"{this.Major}.{this.Minor}.{this.Patch}" + : $"{this.Major}.{this.Minor}"; + + // tag + string tag = this.Tag; + if (tag != null) + result += $"-{tag}"; + return result; + } + + /// Parse a version string without throwing an exception if it fails. + /// The version string. + /// The parsed representation. + /// Returns whether parsing the version succeeded. + internal static bool TryParse(string version, out SemanticVersionImpl parsed) + { + try + { + parsed = new SemanticVersionImpl(version); + return true; + } + catch + { + parsed = null; + return false; + } + } + + + /********* + ** Private methods + *********/ + /// Get a normalised build tag. + /// The tag to normalise. + private string GetNormalisedTag(string tag) + { + tag = tag?.Trim(); + if (string.IsNullOrWhiteSpace(tag) || tag == "0") // '0' from incorrect examples in old SMAPI documentation + return null; + return tag; + } + } +} diff --git a/src/SMAPI.Common/StardewModdingAPI.Common.projitems b/src/SMAPI.Common/StardewModdingAPI.Common.projitems index b3296570..223b0d5c 100644 --- a/src/SMAPI.Common/StardewModdingAPI.Common.projitems +++ b/src/SMAPI.Common/StardewModdingAPI.Common.projitems @@ -11,6 +11,7 @@ + diff --git a/src/SMAPI/SemanticVersion.cs b/src/SMAPI/SemanticVersion.cs index 1b99dae6..ce86dceb 100644 --- a/src/SMAPI/SemanticVersion.cs +++ b/src/SMAPI/SemanticVersion.cs @@ -1,6 +1,6 @@ using System; -using System.Text.RegularExpressions; using Newtonsoft.Json; +using StardewModdingAPI.Common; namespace StardewModdingAPI { @@ -10,31 +10,24 @@ namespace StardewModdingAPI /********* ** Properties *********/ - /// A regular expression matching a semantic version string. - /// - /// This pattern is derived from the BNF documentation in the semver repo, - /// with three important deviations intended to support Stardew Valley mod conventions: - /// - allows short-form "x.y" versions; - /// - allows hyphens in prerelease tags as synonyms for dots (like "-unofficial-update.3"); - /// - doesn't allow '+build' suffixes. - /// - private static readonly Regex Regex = new Regex(@"^(?>(?0|[1-9]\d*))\.(?>(?0|[1-9]\d*))(?>(?:\.(?0|[1-9]\d*))?)(?:-(?(?>[a-z0-9]+[\-\.]?)+))?$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.ExplicitCapture); + /// The underlying semantic version implementation. + private readonly SemanticVersionImpl Version; /********* ** Accessors *********/ /// The major version incremented for major API changes. - public int MajorVersion { get; } + public int MajorVersion => this.Version.Major; /// The minor version incremented for backwards-compatible changes. - public int MinorVersion { get; } + public int MinorVersion => this.Version.Minor; /// The patch version for backwards-compatible bug fixes. - public int PatchVersion { get; } + public int PatchVersion => this.Version.Patch; /// An optional build tag. - public string Build { get; } + public string Build => this.Version.Tag; /********* @@ -47,32 +40,14 @@ namespace StardewModdingAPI /// An optional build tag. [JsonConstructor] public SemanticVersion(int majorVersion, int minorVersion, int patchVersion, string build = null) - { - this.MajorVersion = majorVersion; - this.MinorVersion = minorVersion; - this.PatchVersion = patchVersion; - this.Build = this.GetNormalisedTag(build); - } + : this(new SemanticVersionImpl(majorVersion, minorVersion, patchVersion, build)) { } /// Construct an instance. /// The semantic version string. /// The is null. /// The is not a valid semantic version. public SemanticVersion(string version) - { - // parse - if (version == null) - throw new ArgumentNullException(nameof(version), "The input version string can't be null."); - var match = SemanticVersion.Regex.Match(version.Trim()); - if (!match.Success) - throw new FormatException($"The input '{version}' isn't a valid semantic version."); - - // initialise - this.MajorVersion = int.Parse(match.Groups["major"].Value); - this.MinorVersion = match.Groups["minor"].Success ? int.Parse(match.Groups["minor"].Value) : 0; - this.PatchVersion = match.Groups["patch"].Success ? int.Parse(match.Groups["patch"].Value) : 0; - this.Build = match.Groups["prerelease"].Success ? this.GetNormalisedTag(match.Groups["prerelease"].Value) : null; - } + : this(new SemanticVersionImpl(version)) { } /// Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version. /// The version to compare with this instance. @@ -80,56 +55,7 @@ namespace StardewModdingAPI /// The implementation is defined by Semantic Version 2.0 (http://semver.org/). public int CompareTo(ISemanticVersion other) { - if (other == null) - throw new ArgumentNullException(nameof(other)); - - const int same = 0; - const int curNewer = 1; - const int curOlder = -1; - - // compare stable versions - if (this.MajorVersion != other.MajorVersion) - return this.MajorVersion.CompareTo(other.MajorVersion); - if (this.MinorVersion != other.MinorVersion) - return this.MinorVersion.CompareTo(other.MinorVersion); - if (this.PatchVersion != other.PatchVersion) - return this.PatchVersion.CompareTo(other.PatchVersion); - if (this.Build == other.Build) - return same; - - // stable supercedes pre-release - bool curIsStable = string.IsNullOrWhiteSpace(this.Build); - bool otherIsStable = string.IsNullOrWhiteSpace(other.Build); - if (curIsStable) - return curNewer; - if (otherIsStable) - return curOlder; - - // compare two pre-release tag values - string[] curParts = this.Build.Split('.', '-'); - string[] otherParts = other.Build.Split('.', '-'); - for (int i = 0; i < curParts.Length; i++) - { - // longer prerelease tag supercedes if otherwise equal - if (otherParts.Length <= i) - return curNewer; - - // compare if different - if (curParts[i] != otherParts[i]) - { - // compare numerically if possible - { - if (int.TryParse(curParts[i], out int curNum) && int.TryParse(otherParts[i], out int otherNum)) - return curNum.CompareTo(otherNum); - } - - // else compare lexically - return string.Compare(curParts[i], otherParts[i], StringComparison.OrdinalIgnoreCase); - } - } - - // fallback (this should never happen) - return string.Compare(this.ToString(), other.ToString(), StringComparison.InvariantCultureIgnoreCase); + return this.Version.CompareTo(other.MajorVersion, other.MinorVersion, other.PatchVersion, other.Build); } /// Get whether this version is older than the specified version. @@ -190,16 +116,7 @@ namespace StardewModdingAPI /// Get a string representation of the version. public override string ToString() { - // version - string result = this.PatchVersion != 0 - ? $"{this.MajorVersion}.{this.MinorVersion}.{this.PatchVersion}" - : $"{this.MajorVersion}.{this.MinorVersion}"; - - // tag - string tag = this.Build; - if (tag != null) - result += $"-{tag}"; - return result; + return this.Version.ToString(); } /// Parse a version string without throwing an exception if it fails. @@ -208,30 +125,26 @@ namespace StardewModdingAPI /// Returns whether parsing the version succeeded. internal static bool TryParse(string version, out ISemanticVersion parsed) { - try + if (SemanticVersionImpl.TryParse(version, out SemanticVersionImpl versionImpl)) { - parsed = new SemanticVersion(version); + parsed = new SemanticVersion(versionImpl); return true; } - catch - { - parsed = null; - return false; - } + + parsed = null; + return false; } /********* ** Private methods *********/ - /// Get a normalised build tag. - /// The tag to normalise. - private string GetNormalisedTag(string tag) + /// Construct an instance. + /// The underlying semantic version implementation. + private SemanticVersion(SemanticVersionImpl version) { - tag = tag?.Trim(); - if (string.IsNullOrWhiteSpace(tag) || tag == "0") // '0' from incorrect examples in old SMAPI documentation - return null; - return tag; + this.Version = version; } + } } From 24428d440592b388e8fcfdd87d953b2162eb1af5 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 8 Oct 2017 00:11:50 -0400 Subject: [PATCH 147/186] fix duplicate semver regex --- src/SMAPI.Common/SemanticVersionImpl.cs | 23 ++++++++------------ src/SMAPI.Web/Framework/VersionConstraint.cs | 3 ++- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/SMAPI.Common/SemanticVersionImpl.cs b/src/SMAPI.Common/SemanticVersionImpl.cs index c257aaaf..e0f68a7e 100644 --- a/src/SMAPI.Common/SemanticVersionImpl.cs +++ b/src/SMAPI.Common/SemanticVersionImpl.cs @@ -8,20 +8,6 @@ namespace StardewModdingAPI.Common /// The implementation is defined by Semantic Version 2.0 (http://semver.org/). internal class SemanticVersionImpl { - /********* - ** Properties - *********/ - /// A regular expression matching a semantic version string. - /// - /// This pattern is derived from the BNF documentation in the semver repo, - /// with three important deviations intended to support Stardew Valley mod conventions: - /// - allows short-form "x.y" versions; - /// - allows hyphens in prerelease tags as synonyms for dots (like "-unofficial-update.3"); - /// - doesn't allow '+build' suffixes. - /// - private static readonly Regex Regex = new Regex(@"^(?>(?0|[1-9]\d*))\.(?>(?0|[1-9]\d*))(?>(?:\.(?0|[1-9]\d*))?)(?:-(?(?>[a-z0-9]+[\-\.]?)+))?$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.ExplicitCapture); - - /********* ** Accessors *********/ @@ -37,6 +23,15 @@ namespace StardewModdingAPI.Common /// An optional prerelease tag. public string Tag { get; } + /// A regular expression matching a semantic version string. + /// + /// This pattern is derived from the BNF documentation in the semver repo, + /// with three important deviations intended to support Stardew Valley mod conventions: + /// - allows short-form "x.y" versions; + /// - allows hyphens in prerelease tags as synonyms for dots (like "-unofficial-update.3"); + /// - doesn't allow '+build' suffixes. + /// + internal static readonly Regex Regex = new Regex(@"^(?>(?0|[1-9]\d*))\.(?>(?0|[1-9]\d*))(?>(?:\.(?0|[1-9]\d*))?)(?:-(?(?>[a-z0-9]+[\-\.]?)+))?$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.ExplicitCapture); /********* ** Public methods diff --git a/src/SMAPI.Web/Framework/VersionConstraint.cs b/src/SMAPI.Web/Framework/VersionConstraint.cs index be9c0918..cffb1092 100644 --- a/src/SMAPI.Web/Framework/VersionConstraint.cs +++ b/src/SMAPI.Web/Framework/VersionConstraint.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Routing.Constraints; +using StardewModdingAPI.Common; namespace StardewModdingAPI.Web.Framework { @@ -10,6 +11,6 @@ namespace StardewModdingAPI.Web.Framework *********/ /// Construct an instance. public VersionConstraint() - : base(@"^v(?>(?0|[1-9]\d*))\.(?>(?0|[1-9]\d*))(?>(?:\.(?0|[1-9]\d*))?)(?:-(?(?>[a-z0-9]+[\-\.]?)+))?$") { } + : base(SemanticVersionImpl.Regex) { } } } From af68910685326a660cc88cd92582b38cbc0d9b2f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 8 Oct 2017 00:20:13 -0400 Subject: [PATCH 148/186] convert mod build config into .NET project to simplify C# build tasks --- .../Properties/AssemblyInfo.cs | 9 ++++ .../README.md | 0 .../StardewModdingAPI.ModBuildConfig.csproj | 49 ++++++++++++++++++ .../assets/nuget-icon.pdn | Bin .../assets/nuget-icon.png | Bin .../build/smapi.targets | 0 .../package.nuspec | 0 .../release-notes.md | 0 src/SMAPI.sln | 12 +++++ 9 files changed, 70 insertions(+) create mode 100644 src/SMAPI.ModBuildConfig/Properties/AssemblyInfo.cs rename src/{ModBuildConfig => SMAPI.ModBuildConfig}/README.md (100%) create mode 100644 src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj rename src/{ModBuildConfig => SMAPI.ModBuildConfig}/assets/nuget-icon.pdn (100%) rename src/{ModBuildConfig => SMAPI.ModBuildConfig}/assets/nuget-icon.png (100%) rename src/{ModBuildConfig => SMAPI.ModBuildConfig}/build/smapi.targets (100%) rename src/{ModBuildConfig => SMAPI.ModBuildConfig}/package.nuspec (100%) rename src/{ModBuildConfig => SMAPI.ModBuildConfig}/release-notes.md (100%) diff --git a/src/SMAPI.ModBuildConfig/Properties/AssemblyInfo.cs b/src/SMAPI.ModBuildConfig/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..57dde638 --- /dev/null +++ b/src/SMAPI.ModBuildConfig/Properties/AssemblyInfo.cs @@ -0,0 +1,9 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("StardewModdingAPI.ModBuildConfig")] +[assembly: AssemblyDescription("")] +[assembly: Guid("ea4f1e80-743f-4a1d-9757-ae66904a196a")] +[assembly: ComVisible(false)] +[assembly: AssemblyVersion("1.7.1.0")] +[assembly: AssemblyFileVersion("1.7.1.0")] diff --git a/src/ModBuildConfig/README.md b/src/SMAPI.ModBuildConfig/README.md similarity index 100% rename from src/ModBuildConfig/README.md rename to src/SMAPI.ModBuildConfig/README.md diff --git a/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj b/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj new file mode 100644 index 00000000..2e37a89d --- /dev/null +++ b/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj @@ -0,0 +1,49 @@ + + + + + Debug + x86 + {EA4F1E80-743F-4A1D-9757-AE66904A196A} + Library + Properties + StardewModdingAPI.ModBuildConfig + StardewModdingAPI.ModBuildConfig + v4.6.1 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/ModBuildConfig/assets/nuget-icon.pdn b/src/SMAPI.ModBuildConfig/assets/nuget-icon.pdn similarity index 100% rename from src/ModBuildConfig/assets/nuget-icon.pdn rename to src/SMAPI.ModBuildConfig/assets/nuget-icon.pdn diff --git a/src/ModBuildConfig/assets/nuget-icon.png b/src/SMAPI.ModBuildConfig/assets/nuget-icon.png similarity index 100% rename from src/ModBuildConfig/assets/nuget-icon.png rename to src/SMAPI.ModBuildConfig/assets/nuget-icon.png diff --git a/src/ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets similarity index 100% rename from src/ModBuildConfig/build/smapi.targets rename to src/SMAPI.ModBuildConfig/build/smapi.targets diff --git a/src/ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec similarity index 100% rename from src/ModBuildConfig/package.nuspec rename to src/SMAPI.ModBuildConfig/package.nuspec diff --git a/src/ModBuildConfig/release-notes.md b/src/SMAPI.ModBuildConfig/release-notes.md similarity index 100% rename from src/ModBuildConfig/release-notes.md rename to src/SMAPI.ModBuildConfig/release-notes.md diff --git a/src/SMAPI.sln b/src/SMAPI.sln index 7941394d..d43ba1c2 100644 --- a/src/SMAPI.sln +++ b/src/SMAPI.sln @@ -45,6 +45,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{09CF91E5 ..\build\prepare-install-package.targets = ..\build\prepare-install-package.targets EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.ModBuildConfig", "SMAPI.ModBuildConfig\StardewModdingAPI.ModBuildConfig.csproj", "{EA4F1E80-743F-4A1D-9757-AE66904A196A}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution SMAPI.Common\StardewModdingAPI.Common.projitems*{2aa02fb6-ff03-41cf-a215-2ee60ab4f5dc}*SharedItemsImports = 13 @@ -121,6 +123,16 @@ Global {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|Mixed Platforms.Build.0 = Release|Any CPU {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|x86.ActiveCfg = Release|Any CPU {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|x86.Build.0 = Release|Any CPU + {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Debug|Any CPU.ActiveCfg = Debug|x86 + {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 + {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Debug|Mixed Platforms.Build.0 = Debug|x86 + {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Debug|x86.ActiveCfg = Debug|x86 + {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Debug|x86.Build.0 = Debug|x86 + {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Release|Any CPU.ActiveCfg = Release|x86 + {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Release|Mixed Platforms.ActiveCfg = Release|x86 + {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Release|Mixed Platforms.Build.0 = Release|x86 + {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Release|x86.ActiveCfg = Release|x86 + {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Release|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From ddad601de3610a56a87c3f943d7ecc9c92af15f9 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 8 Oct 2017 01:27:52 -0400 Subject: [PATCH 149/186] move create-zip task into project code --- .../StardewModdingAPI.ModBuildConfig.csproj | 18 ++- .../Tasks/CreateModReleaseZip.cs | 103 +++++++++++++++ src/SMAPI.ModBuildConfig/build/smapi.targets | 120 +----------------- src/SMAPI.ModBuildConfig/package.nuspec | 1 + 4 files changed, 120 insertions(+), 122 deletions(-) create mode 100644 src/SMAPI.ModBuildConfig/Tasks/CreateModReleaseZip.cs diff --git a/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj b/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj index 2e37a89d..9b68fe57 100644 --- a/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj +++ b/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj @@ -16,7 +16,7 @@ true full false - bin\Debug\ + bin\ DEBUG;TRACE prompt 4 @@ -24,21 +24,31 @@ pdbonly true - bin\Release\ + bin\ TRACE prompt 4 + + + + + + - - + + Designer + + + Designer + diff --git a/src/SMAPI.ModBuildConfig/Tasks/CreateModReleaseZip.cs b/src/SMAPI.ModBuildConfig/Tasks/CreateModReleaseZip.cs new file mode 100644 index 00000000..01053860 --- /dev/null +++ b/src/SMAPI.ModBuildConfig/Tasks/CreateModReleaseZip.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Web.Script.Serialization; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace StardewModdingAPI.ModBuildConfig.Tasks +{ + /// A build task which packs mod files into a conventional release zip. + public class CreateModReleaseZip : Task + { + /********* + ** Accessors + *********/ + /// The mod files to pack. + [Required] + public ITaskItem[] Files { get; set; } + + /// The name of the mod. + [Required] + public string ModName { get; set; } + + /// The absolute or relative path to the folder which should contain the generated zip file. + [Required] + public string OutputFolderPath { get; set; } + + + /********* + ** Public methods + *********/ + public override bool Execute() + { + try + { + // create output path if needed + Directory.CreateDirectory(this.OutputFolderPath); + + // get zip filename + string fileName = string.Format("{0}-{1}.zip", this.ModName, this.GetManifestVersion()); + + // clear old zip file if present + string zipPath = Path.Combine(this.OutputFolderPath, fileName); + if (File.Exists(zipPath)) + File.Delete(zipPath); + + // create zip file + using (Stream zipStream = new FileStream(zipPath, FileMode.Create, FileAccess.Write)) + using (ZipArchive archive = new ZipArchive(zipStream, ZipArchiveMode.Create)) + { + foreach (ITaskItem file in this.Files) + { + // get file info + string filePath = file.ItemSpec; + string entryName = this.ModName + '/' + file.GetMetadata("RecursiveDir") + file.GetMetadata("Filename") + file.GetMetadata("Extension"); + if (new FileInfo(filePath).Directory.Name.Equals("i18n", StringComparison.InvariantCultureIgnoreCase)) + entryName = Path.Combine("i18n", entryName); + + // add to zip + using (Stream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read)) + using (Stream fileStreamInZip = archive.CreateEntry(entryName).Open()) + { + fileStream.CopyTo(fileStreamInZip); + } + } + } + + return true; + } + catch (Exception ex) + { + this.Log.LogErrorFromException(ex); + return false; + } + } + + /// Get a semantic version from the mod manifest (if available). + public string GetManifestVersion() + { + // Get the file JSON string + string json = ""; + foreach (ITaskItem file in this.Files) + { + if (Path.GetFileName(file.ItemSpec).ToLower() != "manifest.json") + continue; + json = File.ReadAllText(file.ItemSpec); + break; + } + + // Serialize the manifest json into a data object, then get a version object from that. + IDictionary data = (IDictionary)new JavaScriptSerializer().DeserializeObject(json); + IDictionary version = (IDictionary)data["Version"]; + + // Store our version numbers for ease of use + int major = (int)version["MajorVersion"]; + int minor = (int)version["MinorVersion"]; + int patch = (int)version["PatchVersion"]; + + return String.Format("{0}.{1}.{2}", major, minor, patch); + } + } +} diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets index a1b6aab3..b4bc8d8b 100644 --- a/src/SMAPI.ModBuildConfig/build/smapi.targets +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -1,124 +1,8 @@ - - - - - - - - - - - - - A build task which packs mod files into a conventional release zip. - public class CreateModReleaseZip : Task, ITask - { - /********* - ** Accessors - *********/ - /// The mod files to pack. - public ITaskItem[] Files { get; set; } - - /// The name of the mod. - public string ModName { get; set; } - - /// The absolute or relative path to the folder which should contain the generated zip file. - public string OutputFolderPath { get; set; } - - - /********* - ** Public methods - *********/ - public override bool Execute() - { - try - { - // create output path if needed - Directory.CreateDirectory(OutputFolderPath); - - // get zip filename - string fileName = string.Format("{0}-{1}.zip", this.ModName, this.GetManifestVersion()); - - // clear old zip file if present - string zipPath = Path.Combine(OutputFolderPath, fileName); - if (File.Exists(zipPath)) - File.Delete(zipPath); - - // create zip file - using (Stream zipStream = new FileStream(zipPath, FileMode.Create, FileAccess.Write)) - using (ZipArchive archive = new ZipArchive(zipStream, ZipArchiveMode.Create)) - { - foreach (ITaskItem file in Files) - { - // get file info - string filePath = file.ItemSpec; - string entryName = ModName + '/' + file.GetMetadata("RecursiveDir") + file.GetMetadata("Filename") + file.GetMetadata("Extension"); - if (new FileInfo(filePath).Directory.Name.Equals("i18n", StringComparison.InvariantCultureIgnoreCase)) - entryName = Path.Combine("i18n", entryName); - - // add to zip - using (Stream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read)) - using (Stream fileStreamInZip = archive.CreateEntry(entryName).Open()) - { - fileStream.CopyTo(fileStreamInZip); - } - } - } - - return true; - } - catch (Exception ex) - { - Log.LogErrorFromException(ex); - return false; - } - } - - /// Get a semantic version from the mod manifest (if available). - public string GetManifestVersion() - { - // Get the file JSON string - string json = ""; - foreach(ITaskItem file in Files) - { - if(Path.GetFileName(file.ItemSpec).ToLower() != "manifest.json") - continue; - json = File.ReadAllText(file.ItemSpec); - break; - } - - // Serialize the manifest json into a data object, then get a version object from that. - IDictionary data = (IDictionary)new JavaScriptSerializer().DeserializeObject(json); - IDictionary version = (IDictionary)data["Version"]; - - // Store our version numbers for ease of use - int major = (int)version["MajorVersion"]; - int minor = (int)version["MinorVersion"]; - int patch = (int)version["PatchVersion"]; - - return String.Format("{0}.{1}.{2}", major, minor, patch); - } - } - ]]> - - - - + - + - + From cd93382c645da3c6d3ce4e532307f42704ba4c76 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 8 Oct 2017 15:03:31 -0400 Subject: [PATCH 159/186] move zip logic into method --- src/SMAPI.ModBuildConfig/DeployModTask.cs | 79 ++++++++++++-------- src/SMAPI.ModBuildConfig/build/smapi.targets | 2 +- 2 files changed, 48 insertions(+), 33 deletions(-) diff --git a/src/SMAPI.ModBuildConfig/DeployModTask.cs b/src/SMAPI.ModBuildConfig/DeployModTask.cs index 0483f651..2018ab06 100644 --- a/src/SMAPI.ModBuildConfig/DeployModTask.cs +++ b/src/SMAPI.ModBuildConfig/DeployModTask.cs @@ -33,7 +33,7 @@ namespace StardewModdingAPI.ModBuildConfig /// The absolute or relative path to the folder which should contain the generated zip file. [Required] - public string OutputFolderPath { get; set; } + public string ModZipPath { get; set; } /********* @@ -45,32 +45,8 @@ namespace StardewModdingAPI.ModBuildConfig { try { - // get names - string zipName = this.EscapeInvalidFilenameCharacters($"{this.ModName}-{this.GetManifestVersion()}.zip"); - string folderName = this.EscapeInvalidFilenameCharacters(this.ModName); - string zipPath = Path.Combine(this.OutputFolderPath, zipName); - - // create zip file - Directory.CreateDirectory(this.OutputFolderPath); - using (Stream zipStream = new FileStream(zipPath, FileMode.Create, FileAccess.Write)) - using (ZipArchive archive = new ZipArchive(zipStream, ZipArchiveMode.Create)) - { - foreach (ITaskItem file in this.Files) - { - // get file info - string filePath = file.ItemSpec; - string entryName = folderName + '/' + file.GetMetadata("RecursiveDir") + file.GetMetadata("Filename") + file.GetMetadata("Extension"); - if (new FileInfo(filePath).Directory.Name.Equals("i18n", StringComparison.InvariantCultureIgnoreCase)) - entryName = Path.Combine("i18n", entryName); - - // add to zip - using (Stream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read)) - using (Stream fileStreamInZip = archive.CreateEntry(entryName).Open()) - { - fileStream.CopyTo(fileStreamInZip); - } - } - } + string modVersion = this.GetManifestVersion(); + this.CreateReleaseZip(this.Files, this.ModName, modVersion, this.ModZipPath); return true; } @@ -81,9 +57,48 @@ namespace StardewModdingAPI.ModBuildConfig } } + + /********* + ** Private methods + *********/ + /// Create a release zip in the recommended format for uploading to mod sites. + /// The files to include. + /// The name of the mod. + /// The mod version string. + /// The absolute or relative path to the folder which should contain the generated zip file. + private void CreateReleaseZip(ITaskItem[] files, string modName, string modVersion, string outputFolderPath) + { + // get names + string zipName = this.EscapeInvalidFilenameCharacters($"{modName}-{modVersion}.zip"); + string folderName = this.EscapeInvalidFilenameCharacters(modName); + string zipPath = Path.Combine(outputFolderPath, zipName); + + // create zip file + Directory.CreateDirectory(outputFolderPath); + using (Stream zipStream = new FileStream(zipPath, FileMode.Create, FileAccess.Write)) + using (ZipArchive archive = new ZipArchive(zipStream, ZipArchiveMode.Create)) + { + foreach (ITaskItem file in files) + { + // get file info + string filePath = file.ItemSpec; + string entryName = folderName + '/' + file.GetMetadata("RecursiveDir") + file.GetMetadata("Filename") + file.GetMetadata("Extension"); + if (new FileInfo(filePath).Directory.Name.Equals("i18n", StringComparison.InvariantCultureIgnoreCase)) + entryName = Path.Combine("i18n", entryName); + + // add to zip + using (Stream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read)) + using (Stream fileStreamInZip = archive.CreateEntry(entryName).Open()) + { + fileStream.CopyTo(fileStreamInZip); + } + } + } + } + /// Get a semantic version from the mod manifest (if available). /// The manifest file wasn't found or is invalid. - public string GetManifestVersion() + private string GetManifestVersion() { // find manifest file ITaskItem file = this.Files.FirstOrDefault(p => this.ManifestFileName.Equals(Path.GetFileName(p.ItemSpec), StringComparison.InvariantCultureIgnoreCase)); @@ -114,10 +129,10 @@ namespace StardewModdingAPI.ModBuildConfig // get version string if (versionObj is IDictionary versionFields) // SMAPI 1.x { - int major = versionFields.ContainsKey("MajorVersion") ? (int) versionFields["MajorVersion"] : 0; - int minor = versionFields.ContainsKey("MinorVersion") ? (int) versionFields["MinorVersion"] : 0; - int patch = versionFields.ContainsKey("PatchVersion") ? (int) versionFields["PatchVersion"] : 0; - string tag = versionFields.ContainsKey("Build") ? (string) versionFields["Build"] : null; + int major = versionFields.ContainsKey("MajorVersion") ? (int)versionFields["MajorVersion"] : 0; + int minor = versionFields.ContainsKey("MinorVersion") ? (int)versionFields["MinorVersion"] : 0; + int patch = versionFields.ContainsKey("PatchVersion") ? (int)versionFields["PatchVersion"] : 0; + string tag = versionFields.ContainsKey("Build") ? (string)versionFields["Build"] : null; return new SemanticVersionImpl(major, minor, patch, tag).ToString(); } return new SemanticVersionImpl(versionObj.ToString()).ToString(); // SMAPI 2.0+ diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets index 61bf96ac..46e8428d 100644 --- a/src/SMAPI.ModBuildConfig/build/smapi.targets +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -152,6 +152,6 @@ - + From 475efa12febcb1f1f0976cb6c84e445a263daed9 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 8 Oct 2017 18:05:47 -0400 Subject: [PATCH 160/186] rewrite mod build package per new docs --- docs/mod-build-config.md | 14 +- src/SMAPI.ModBuildConfig/DeployModTask.cs | 176 ++++++++++-------- .../Framework/ModFileManager.cs | 162 ++++++++++++++++ .../Framework/UserErrorException.cs | 16 ++ .../StardewModdingAPI.ModBuildConfig.csproj | 3 + src/SMAPI.ModBuildConfig/build/smapi.targets | 76 +++----- 6 files changed, 313 insertions(+), 134 deletions(-) create mode 100644 src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs create mode 100644 src/SMAPI.ModBuildConfig/Framework/UserErrorException.cs diff --git a/docs/mod-build-config.md b/docs/mod-build-config.md index b091c2c0..f02981b9 100644 --- a/docs/mod-build-config.md +++ b/docs/mod-build-config.md @@ -71,6 +71,11 @@ Finally, you can disable the zip creation with this: False ``` +Or only create it in release builds with this: +```xml +False +``` + ### Game path The package usually detects where your game is installed automatically. If it can't find your game or you have multiple installs, you can specify the path yourself. There's two ways to do that: @@ -118,14 +123,15 @@ still compile on a different computer). ## Troubleshoot ### "Failed to find the game install path" -That error means the package couldn't find your game. You need to specify the game path yourself; -see _[Game path](#game-path)_ above. +That error means the package couldn't find your game. You can specify the game path yourself; see +_[Game path](#game-path)_ above. ## Release notes ### 2.0 -* Mods are now copied into the `Mods` folder automatically (configurable). -* The release zip is now created automatically in your build output folder (configurable). +* Added: mods are now copied into the `Mods` folder automatically (configurable). +* Added: release zips are now created automatically in your build output folder (configurable). * Added mod's version to release zip filename. +* Improved errors to simplify troubleshooting. * Fixed release zip not having a mod folder. * Fixed release zip failing if mod name contains characters that aren't valid in a filename. diff --git a/src/SMAPI.ModBuildConfig/DeployModTask.cs b/src/SMAPI.ModBuildConfig/DeployModTask.cs index 2018ab06..a693fe32 100644 --- a/src/SMAPI.ModBuildConfig/DeployModTask.cs +++ b/src/SMAPI.ModBuildConfig/DeployModTask.cs @@ -2,11 +2,9 @@ using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; -using System.Linq; -using System.Web.Script.Serialization; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; -using StardewModdingAPI.Common; +using StardewModdingAPI.ModBuildConfig.Framework; namespace StardewModdingAPI.ModBuildConfig { @@ -16,25 +14,53 @@ namespace StardewModdingAPI.ModBuildConfig /********* ** Properties *********/ - /// The name of the manifest file. - private readonly string ManifestFileName = "manifest.json"; + /// The MSBuild platforms recognised by the build configuration. + private readonly HashSet ValidPlatforms = new HashSet(new[] { "OSX", "Unix", "Windows_NT" }, StringComparer.InvariantCultureIgnoreCase); + + /// The name of the game's main executable file. + private string GameExeName => this.Platform == "Windows_NT" + ? "Stardew Valley.exe" + : "StardewValley.exe"; + + /// The name of SMAPI's main executable file. + private readonly string SmapiExeName = "StardewModdingAPI.exe"; /********* ** Accessors *********/ - /// The mod files to pack. + /// The name of the mod folder. [Required] - public ITaskItem[] Files { get; set; } - - /// The name of the mod. - [Required] - public string ModName { get; set; } + public string ModFolderName { get; set; } /// The absolute or relative path to the folder which should contain the generated zip file. [Required] public string ModZipPath { get; set; } + /// The folder containing the project files. + [Required] + public string ProjectDir { get; set; } + + /// The folder containing the build output. + [Required] + public string TargetDir { get; set; } + + /// The folder containing the game files. + [Required] + public string GameDir { get; set; } + + /// The MSBuild OS value. + [Required] + public string Platform { get; set; } + + /// Whether to enable copying the mod files into the game's Mods folder. + [Required] + public bool EnableModDeploy { get; set; } + + /// Whether to enable the release zip. + [Required] + public bool EnableModZip { get; set; } + /********* ** Public methods @@ -43,33 +69,80 @@ namespace StardewModdingAPI.ModBuildConfig /// true if the task successfully executed; otherwise, false. public override bool Execute() { + if (!this.EnableModDeploy && !this.EnableModZip) + return true; // nothing to do + try { - string modVersion = this.GetManifestVersion(); - this.CreateReleaseZip(this.Files, this.ModName, modVersion, this.ModZipPath); + // validate context + if (!this.ValidPlatforms.Contains(this.Platform)) + throw new UserErrorException($"The mod build package doesn't recognise OS type '{this.Platform}'."); + if (!Directory.Exists(this.GameDir)) + throw new UserErrorException("The mod build package can't find your game path. See https://github.com/Pathoschild/SMAPI/blob/develop/docs/mod-build-config.md for help specifying it."); + if (!File.Exists(Path.Combine(this.GameDir, this.GameExeName))) + throw new UserErrorException($"The mod build package found a game folder at {this.GameDir}, but it doesn't contain the {this.GameExeName} file. If this folder is invalid, delete it and the package will autodetect another game install path."); + if (!File.Exists(Path.Combine(this.GameDir, this.SmapiExeName))) + throw new UserErrorException($"The mod build package found a game folder at {this.GameDir}, but it doesn't contain SMAPI. You need to install SMAPI before building the mod."); + + // get mod info + ModFileManager package = new ModFileManager(this.ProjectDir, this.TargetDir); + + // deploy mod files + if (this.EnableModDeploy) + { + string outputPath = Path.Combine(this.GameDir, "Mods", this.EscapeInvalidFilenameCharacters(this.ModFolderName)); + this.Log.LogMessage(MessageImportance.High, $"The mod build package is copying the mod files to {outputPath}..."); + this.CreateModFolder(package.GetFiles(), outputPath); + } + + // create release zip + if (this.EnableModZip) + { + this.Log.LogMessage(MessageImportance.High, $"The mod build package is generating a release zip at {this.ModZipPath} for {this.ModFolderName}..."); + this.CreateReleaseZip(package.GetFiles(), this.ModFolderName, package.GetManifestVersion(), this.ModZipPath); + } return true; } - catch (Exception ex) + catch (UserErrorException ex) { this.Log.LogErrorFromException(ex); return false; } + catch (Exception ex) + { + this.Log.LogError($"The mod build package failed trying to deploy the mod.\n{ex}"); + return false; + } } /********* ** Private methods *********/ + /// Copy the mod files into the game's mod folder. + /// The files to include. + /// The folder path to create with the mod files. + private void CreateModFolder(IDictionary files, string modFolderPath) + { + Directory.CreateDirectory(modFolderPath); + foreach (var entry in files) + { + string fromPath = entry.Value.FullName; + string toPath = Path.Combine(modFolderPath, entry.Key); + File.Copy(fromPath, toPath, overwrite: true); + } + } + /// Create a release zip in the recommended format for uploading to mod sites. /// The files to include. /// The name of the mod. /// The mod version string. /// The absolute or relative path to the folder which should contain the generated zip file. - private void CreateReleaseZip(ITaskItem[] files, string modName, string modVersion, string outputFolderPath) + private void CreateReleaseZip(IDictionary files, string modName, string modVersion, string outputFolderPath) { // get names - string zipName = this.EscapeInvalidFilenameCharacters($"{modName}-{modVersion}.zip"); + string zipName = this.EscapeInvalidFilenameCharacters($"{modName} {modVersion}.zip"); string folderName = this.EscapeInvalidFilenameCharacters(modName); string zipPath = Path.Combine(outputFolderPath, zipName); @@ -78,84 +151,25 @@ namespace StardewModdingAPI.ModBuildConfig using (Stream zipStream = new FileStream(zipPath, FileMode.Create, FileAccess.Write)) using (ZipArchive archive = new ZipArchive(zipStream, ZipArchiveMode.Create)) { - foreach (ITaskItem file in files) + foreach (var fileEntry in files) { + string relativePath = fileEntry.Key; + FileInfo file = fileEntry.Value; + // get file info - string filePath = file.ItemSpec; - string entryName = folderName + '/' + file.GetMetadata("RecursiveDir") + file.GetMetadata("Filename") + file.GetMetadata("Extension"); + string filePath = file.FullName; + string entryName = folderName + '/' + relativePath.Replace(Path.DirectorySeparatorChar, '/'); if (new FileInfo(filePath).Directory.Name.Equals("i18n", StringComparison.InvariantCultureIgnoreCase)) entryName = Path.Combine("i18n", entryName); // add to zip using (Stream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read)) using (Stream fileStreamInZip = archive.CreateEntry(entryName).Open()) - { fileStream.CopyTo(fileStreamInZip); - } } } } - /// Get a semantic version from the mod manifest (if available). - /// The manifest file wasn't found or is invalid. - private string GetManifestVersion() - { - // find manifest file - ITaskItem file = this.Files.FirstOrDefault(p => this.ManifestFileName.Equals(Path.GetFileName(p.ItemSpec), StringComparison.InvariantCultureIgnoreCase)); - if (file == null) - throw new InvalidOperationException($"The mod must include a {this.ManifestFileName} file."); - - // read content - string json = File.ReadAllText(file.ItemSpec); - if (string.IsNullOrWhiteSpace(json)) - throw new InvalidOperationException($"The mod's {this.ManifestFileName} file must not be empty."); - - // parse JSON - IDictionary data; - try - { - data = this.Parse(json); - } - catch (Exception ex) - { - throw new InvalidOperationException($"The mod's {this.ManifestFileName} couldn't be parsed. It doesn't seem to be valid JSON.", ex); - } - - // get version field - object versionObj = data.ContainsKey("Version") ? data["Version"] : null; - if (versionObj == null) - throw new InvalidOperationException($"The mod's {this.ManifestFileName} must have a version field."); - - // get version string - if (versionObj is IDictionary versionFields) // SMAPI 1.x - { - int major = versionFields.ContainsKey("MajorVersion") ? (int)versionFields["MajorVersion"] : 0; - int minor = versionFields.ContainsKey("MinorVersion") ? (int)versionFields["MinorVersion"] : 0; - int patch = versionFields.ContainsKey("PatchVersion") ? (int)versionFields["PatchVersion"] : 0; - string tag = versionFields.ContainsKey("Build") ? (string)versionFields["Build"] : null; - return new SemanticVersionImpl(major, minor, patch, tag).ToString(); - } - return new SemanticVersionImpl(versionObj.ToString()).ToString(); // SMAPI 2.0+ - } - - /// Get a case-insensitive dictionary matching the given JSON. - /// The JSON to parse. - private IDictionary Parse(string json) - { - IDictionary MakeCaseInsensitive(IDictionary dict) - { - foreach (var field in dict.ToArray()) - { - if (field.Value is IDictionary value) - dict[field.Key] = MakeCaseInsensitive(value); - } - return new Dictionary(dict, StringComparer.InvariantCultureIgnoreCase); - } - - IDictionary data = (IDictionary)new JavaScriptSerializer().DeserializeObject(json); - return MakeCaseInsensitive(data); - } - /// Get a copy of a filename with all invalid filename characters substituted. /// The filename. private string EscapeInvalidFilenameCharacters(string name) diff --git a/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs b/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs new file mode 100644 index 00000000..9d9054f1 --- /dev/null +++ b/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Web.Script.Serialization; +using StardewModdingAPI.Common; + +namespace StardewModdingAPI.ModBuildConfig.Framework +{ + /// Manages the files that are part of a mod package. + internal class ModFileManager + { + /********* + ** Properties + *********/ + /// The name of the manifest file. + private readonly string ManifestFileName = "manifest.json"; + + /// The files that are part of the package. + private readonly IDictionary Files; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The folder containing the project files. + /// The folder containing the build output. + /// The mod package isn't valid. + public ModFileManager(string projectDir, string targetDir) + { + this.Files = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + + // validate paths + if (!Directory.Exists(projectDir)) + throw new UserErrorException("Could not create mod package because the project folder wasn't found."); + if (!Directory.Exists(targetDir)) + throw new UserErrorException("Could not create mod package because no build output was found."); + + // project manifest + bool hasProjectManifest = false; + { + FileInfo manifest = new FileInfo(Path.Combine(projectDir, "manifest.json")); + if (manifest.Exists) + { + this.Files[this.ManifestFileName] = manifest; + hasProjectManifest = true; + } + } + + // project i18n files + bool hasProjectTranslations = false; + DirectoryInfo translationsFolder = new DirectoryInfo(Path.Combine(projectDir, "i18n")); + if (translationsFolder.Exists) + { + foreach (FileInfo file in translationsFolder.EnumerateFiles()) + this.Files[Path.Combine("i18n", file.Name)] = file; + hasProjectTranslations = true; + } + + // build output + DirectoryInfo buildFolder = new DirectoryInfo(targetDir); + foreach (FileInfo file in buildFolder.EnumerateFiles("*", SearchOption.AllDirectories)) + { + // get relative paths + string relativePath = file.FullName.Replace(buildFolder.FullName, ""); + string relativeDirPath = file.Directory.FullName.Replace(buildFolder.FullName, ""); + + // prefer project manifest/i18n files + if (hasProjectManifest && relativePath.Equals(this.ManifestFileName, StringComparison.InvariantCultureIgnoreCase)) + continue; + if (hasProjectTranslations && relativeDirPath.Equals("i18n", StringComparison.InvariantCultureIgnoreCase)) + continue; + + // ignore release zips + if (file.Extension.Equals("zip", StringComparison.InvariantCultureIgnoreCase)) + continue; + + // add file + this.Files[relativePath] = file; + } + + // check for missing manifest + if (!this.Files.ContainsKey(this.ManifestFileName)) + throw new UserErrorException($"Could not create mod package because no {this.ManifestFileName} was found in the project or build output."); + + // check for missing DLL + // ReSharper disable once SimplifyLinqExpression + if (!this.Files.Any(p => !p.Key.EndsWith(".dll"))) + throw new UserErrorException("Could not create mod package because no .dll file was found in the project or build output."); + } + + /// Get the files in the mod package. + public IDictionary GetFiles() + { + return new Dictionary(this.Files, StringComparer.InvariantCultureIgnoreCase); + } + + /// Get a semantic version from the mod manifest. + /// The manifest is missing or invalid. + public string GetManifestVersion() + { + // get manifest file + if (!this.Files.TryGetValue(this.ManifestFileName, out FileInfo manifestFile)) + throw new InvalidOperationException($"The mod does not have a {this.ManifestFileName} file."); // shouldn't happen since we validate in constructor + + // read content + string json = File.ReadAllText(manifestFile.FullName); + if (string.IsNullOrWhiteSpace(json)) + throw new UserErrorException("The mod's manifest must not be empty."); + + // parse JSON + IDictionary data; + try + { + data = this.Parse(json); + } + catch (Exception ex) + { + throw new UserErrorException($"The mod's manifest couldn't be parsed. It doesn't seem to be valid JSON.\n{ex}"); + } + + // get version field + object versionObj = data.ContainsKey("Version") ? data["Version"] : null; + if (versionObj == null) + throw new UserErrorException("The mod's manifest must have a version field."); + + // get version string + if (versionObj is IDictionary versionFields) // SMAPI 1.x + { + int major = versionFields.ContainsKey("MajorVersion") ? (int)versionFields["MajorVersion"] : 0; + int minor = versionFields.ContainsKey("MinorVersion") ? (int)versionFields["MinorVersion"] : 0; + int patch = versionFields.ContainsKey("PatchVersion") ? (int)versionFields["PatchVersion"] : 0; + string tag = versionFields.ContainsKey("Build") ? (string)versionFields["Build"] : null; + return new SemanticVersionImpl(major, minor, patch, tag).ToString(); + } + return new SemanticVersionImpl(versionObj.ToString()).ToString(); // SMAPI 2.0+ + } + + + /********* + ** Private methods + *********/ + /// Get a case-insensitive dictionary matching the given JSON. + /// The JSON to parse. + private IDictionary Parse(string json) + { + IDictionary MakeCaseInsensitive(IDictionary dict) + { + foreach (var field in dict.ToArray()) + { + if (field.Value is IDictionary value) + dict[field.Key] = MakeCaseInsensitive(value); + } + return new Dictionary(dict, StringComparer.InvariantCultureIgnoreCase); + } + + IDictionary data = (IDictionary)new JavaScriptSerializer().DeserializeObject(json); + return MakeCaseInsensitive(data); + } + } +} diff --git a/src/SMAPI.ModBuildConfig/Framework/UserErrorException.cs b/src/SMAPI.ModBuildConfig/Framework/UserErrorException.cs new file mode 100644 index 00000000..64e31c29 --- /dev/null +++ b/src/SMAPI.ModBuildConfig/Framework/UserErrorException.cs @@ -0,0 +1,16 @@ +using System; + +namespace StardewModdingAPI.ModBuildConfig.Framework +{ + /// A user error whose message can be displayed to the user. + internal class UserErrorException : Exception + { + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The error message. + public UserErrorException(string message) + : base(message) { } + } +} diff --git a/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj b/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj index f943bc97..2a445f72 100644 --- a/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj +++ b/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj @@ -39,12 +39,15 @@ + + Designer + PreserveNewest Designer diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets index 46e8428d..0010d8ff 100644 --- a/src/SMAPI.ModBuildConfig/build/smapi.targets +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -7,15 +7,24 @@ - + - + + + + $(DeployModFolderName) + $(DeployModZipTo) + + + $(MSBuildProjectName) + $(TargetDir) + True + True + + + @@ -106,52 +115,21 @@ - - - - + + - - - - - + EnableModDeploy="$(EnableModDeploy)" + EnableModZip="$(EnableModZip)" - - - - - $(GamePath)\Mods\$(DeployModFolderName) - - - - + ProjectDir="$(ProjectDir)" + TargetDir="$(TargetDir)" + GameDir="$(GamePath)" - - - - - - - - - - - - - - - - - - - + Platform="$(OS)" + /> From d47105a27841bcae80fcaa2351a2a658cd3d7fdb Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 8 Oct 2017 21:21:11 -0400 Subject: [PATCH 161/186] update mod build package nuspec --- .../Framework/ModFileManager.cs | 2 +- .../StardewModdingAPI.ModBuildConfig.csproj | 1 - src/SMAPI.ModBuildConfig/package.nuspec | 20 +++++++++++-------- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs b/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs index 9d9054f1..10c55d4c 100644 --- a/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs +++ b/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs @@ -73,7 +73,7 @@ namespace StardewModdingAPI.ModBuildConfig.Framework continue; // ignore release zips - if (file.Extension.Equals("zip", StringComparison.InvariantCultureIgnoreCase)) + if (file.Extension.Equals(".zip", StringComparison.InvariantCultureIgnoreCase)) continue; // add file diff --git a/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj b/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj index 2a445f72..e04f09a7 100644 --- a/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj +++ b/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj @@ -47,7 +47,6 @@ Designer - PreserveNewest Designer diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec index 9d547e28..4242489e 100644 --- a/src/SMAPI.ModBuildConfig/package.nuspec +++ b/src/SMAPI.ModBuildConfig/package.nuspec @@ -2,22 +2,26 @@ Pathoschild.Stardew.ModBuildConfig - 1.7.1 - MSBuild config for Stardew Valley mods + 2.0-alpha + Build package for SMAPI mods Pathoschild Pathoschild false - https://github.com/Pathoschild/Stardew.ModBuildConfig/blob/1.7.1/LICENSE.txt - https://github.com/Pathoschild/Stardew.ModBuildConfig#readme - https://raw.githubusercontent.com/Pathoschild/Stardew.ModBuildConfig/1.7.1/assets/nuget-icon.png + https://github.com/Pathoschild/SMAPI/blob/develop/LICENSE.txt + https://github.com/Pathoschild/SMAPI/blob/develop/docs/mod-build-config.md#readme + https://raw.githubusercontent.com/Pathoschild/SMAPI/develop/src/SMAPI.ModBuildConfig/assets/nuget-icon.png Automates the build configuration for crossplatform Stardew Valley SMAPI mods. - 1.7 added an option to create release zips on build and added a reference to XNA's XACT library for audio-related mods. - 1.7.1 fixed an issue where i18n folders were flattened, and ensures that the manifest/i18n files in the project take precedence over those in the build output if both are present. + - Added: mods are now copied into the `Mods` folder automatically (configurable). + - Added: release zips are now created automatically in your build output folder (configurable). + - Added mod's version to release zip filename. + - Improved errors to simplify troubleshooting. + - Fixed release zip not having a mod folder. + - Fixed release zip failing if mod name contains characters that aren't valid in a filename. + - From dad0d67022be32c6b228f771099cc2379850bf87 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 9 Oct 2017 12:43:41 -0400 Subject: [PATCH 162/186] rm unneeded code --- src/SMAPI.ModBuildConfig/DeployModTask.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/SMAPI.ModBuildConfig/DeployModTask.cs b/src/SMAPI.ModBuildConfig/DeployModTask.cs index a693fe32..0bfe9b2d 100644 --- a/src/SMAPI.ModBuildConfig/DeployModTask.cs +++ b/src/SMAPI.ModBuildConfig/DeployModTask.cs @@ -159,8 +159,6 @@ namespace StardewModdingAPI.ModBuildConfig // get file info string filePath = file.FullName; string entryName = folderName + '/' + relativePath.Replace(Path.DirectorySeparatorChar, '/'); - if (new FileInfo(filePath).Directory.Name.Equals("i18n", StringComparison.InvariantCultureIgnoreCase)) - entryName = Path.Combine("i18n", entryName); // add to zip using (Stream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read)) From c456a0f56ed0fba55042c167afa83c9922a5db33 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 9 Oct 2017 12:44:48 -0400 Subject: [PATCH 163/186] don't include Json.NET in mod deploy or release zip since it's loaded by SMAPI --- docs/mod-build-config.md | 1 + .../Framework/ModFileManager.cs | 20 +++++++++++++++---- src/SMAPI.ModBuildConfig/package.nuspec | 3 ++- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/docs/mod-build-config.md b/docs/mod-build-config.md index f02981b9..ca750c86 100644 --- a/docs/mod-build-config.md +++ b/docs/mod-build-config.md @@ -130,6 +130,7 @@ _[Game path](#game-path)_ above. ### 2.0 * Added: mods are now copied into the `Mods` folder automatically (configurable). * Added: release zips are now created automatically in your build output folder (configurable). +* Added: mod deploy and release zips now exclude Json.NET automatically, since it's provided by SMAPI. * Added mod's version to release zip filename. * Improved errors to simplify troubleshooting. * Fixed release zip not having a mod folder. diff --git a/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs b/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs index 10c55d4c..64262dc2 100644 --- a/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs +++ b/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs @@ -67,15 +67,19 @@ namespace StardewModdingAPI.ModBuildConfig.Framework string relativeDirPath = file.Directory.FullName.Replace(buildFolder.FullName, ""); // prefer project manifest/i18n files - if (hasProjectManifest && relativePath.Equals(this.ManifestFileName, StringComparison.InvariantCultureIgnoreCase)) + if (hasProjectManifest && this.EqualsInvariant(relativePath, this.ManifestFileName)) continue; - if (hasProjectTranslations && relativeDirPath.Equals("i18n", StringComparison.InvariantCultureIgnoreCase)) + if (hasProjectTranslations && this.EqualsInvariant(relativeDirPath, "i18n")) continue; // ignore release zips - if (file.Extension.Equals(".zip", StringComparison.InvariantCultureIgnoreCase)) + if (this.EqualsInvariant(file.Extension, ".zip")) continue; - + + // ignore Json.NET (bundled into SMAPI) + if (this.EqualsInvariant(file.Name, "Newtonsoft.Json.dll") || this.EqualsInvariant(file.Name, "Newtonsoft.Json.xml")) + continue; + // add file this.Files[relativePath] = file; } @@ -158,5 +162,13 @@ namespace StardewModdingAPI.ModBuildConfig.Framework IDictionary data = (IDictionary)new JavaScriptSerializer().DeserializeObject(json); return MakeCaseInsensitive(data); } + + /// Get whether a string is equal to another case-insensitively. + /// The string value. + /// The string to compare with. + private bool EqualsInvariant(string str, string other) + { + return str.Equals(other, StringComparison.InvariantCultureIgnoreCase); + } } } diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec index 4242489e..3ab5db8e 100644 --- a/src/SMAPI.ModBuildConfig/package.nuspec +++ b/src/SMAPI.ModBuildConfig/package.nuspec @@ -2,7 +2,7 @@ Pathoschild.Stardew.ModBuildConfig - 2.0-alpha + 2.0-beta Build package for SMAPI mods Pathoschild Pathoschild @@ -14,6 +14,7 @@ - Added: mods are now copied into the `Mods` folder automatically (configurable). - Added: release zips are now created automatically in your build output folder (configurable). + - Added: mod deploy and release zips now exclude Json.NET automatically, since it's provided by SMAPI. - Added mod's version to release zip filename. - Improved errors to simplify troubleshooting. - Fixed release zip not having a mod folder. From 4f8994a1debdc4f1b2cb39854977e3b00a851992 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 9 Oct 2017 21:03:25 -0400 Subject: [PATCH 164/186] fix update check error --- src/SMAPI.Common/Models/ModInfoModel.cs | 11 +++++++++-- src/SMAPI.Common/Models/ModSeachModel.cs | 8 +++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/SMAPI.Common/Models/ModInfoModel.cs b/src/SMAPI.Common/Models/ModInfoModel.cs index 4daa7064..48305cb8 100644 --- a/src/SMAPI.Common/Models/ModInfoModel.cs +++ b/src/SMAPI.Common/Models/ModInfoModel.cs @@ -22,7 +22,14 @@ namespace StardewModdingAPI.Common.Models /********* ** Public methods *********/ - /// Construct a valid instance. + /// Construct an empty instance. + public ModInfoModel() + { + // needed for JSON deserialising + } + + + /// Construct an instance. /// The mod name. /// The mod's semantic version number. /// The mod's web URL. @@ -35,7 +42,7 @@ namespace StardewModdingAPI.Common.Models this.Error = error; // mainly initialised here for the JSON deserialiser } - /// Construct an valid instance. + /// Construct an instance. /// The error message indicating why the mod is invalid. public ModInfoModel(string error) { diff --git a/src/SMAPI.Common/Models/ModSeachModel.cs b/src/SMAPI.Common/Models/ModSeachModel.cs index 3f69f0ae..13b05d2d 100644 --- a/src/SMAPI.Common/Models/ModSeachModel.cs +++ b/src/SMAPI.Common/Models/ModSeachModel.cs @@ -17,10 +17,12 @@ namespace StardewModdingAPI.Common.Models ** Public methods *********/ /// Construct an empty instance. - /// This constructed is needed for JSON deserialisation. - public ModSearchModel() { } + public ModSearchModel() + { + // needed for JSON deserialising + } - /// Construct an valid instance. + /// Construct an instance. /// The namespaced mod keys to search. public ModSearchModel(IEnumerable modKeys) { From 54128ab482d1c258f120eb8b5bcff4a3be037d97 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 10 Oct 2017 00:02:31 -0400 Subject: [PATCH 165/186] update mod data --- src/SMAPI/StardewModdingAPI.config.json | 27 ++++++++++++++----------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/SMAPI/StardewModdingAPI.config.json b/src/SMAPI/StardewModdingAPI.config.json index ebc1235b..a4829d74 100644 --- a/src/SMAPI/StardewModdingAPI.config.json +++ b/src/SMAPI/StardewModdingAPI.config.json @@ -286,11 +286,15 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~1.3": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } }, + { + // Butcher Mod + "ID": "DIGUS.BUTCHER", + "UpdateKeys": [ "Nexus:1538" ] + }, { // Buy Cooking Recipes "ID": "Denifia.BuyRecipes", "UpdateKeys": [ "Nexus:1126" ], // added in 1.0.1 (2017-10-04) - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -818,7 +822,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha // GenericShopExtender "ID": "GenericShopExtender", "UpdateKeys": [ "Nexus:814" ], // added in 0.1.3 - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~0.1.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -933,7 +936,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha // Improved Quality of Life "ID": "Demiacle.ImprovedQualityOfLife", "UpdateKeys": [ "Nexus:1025" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -1040,6 +1042,11 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "~0.1.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } }, + { + // Mail Framework + "ID": "DIGUS.MailFrameworkMod", + "UpdateKeys": [ "Nexus:1536" ] + }, { // MailOrderPigs "ID": "MailOrderPigs.dll | jwdred.MailOrderPigs", // changed in 1.0.2 @@ -1116,7 +1123,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha // More Rain "ID": "{ID:'4108e859-333c-4fec-a1a7-d2e18c1019fe', Name:'More_Rain'} | Omegasis.MoreRain", // changed in 1.5; disambiguate from other mods by Alpha_Omegasis "UpdateKeys": [ "Nexus:441" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -1239,7 +1245,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha // OmniFarm "ID": "BlueMod_OmniFarm | PhthaloBlue.OmniFarm", // changed in 2.0.2-pathoschild-update "UpdateKeys": [ "GitHub:lambui/StardewValleyMod_OmniFarm" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~2.0.1": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -1381,7 +1386,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha // Replanter "ID": "Replanter.dll | jwdred.Replanter", // changed in 1.0.5 "UpdateKeys": [ "Nexus:589" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -1435,7 +1439,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha // Save Anywhere "ID": "{ID:'SaveAnywhere', Name:'Save Anywhere'} | Omegasis.SaveAnywhere", // changed in 2.5; disambiguate from Night Owl "UpdateKeys": [ "Nexus:444" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~2.4": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 }, @@ -1465,7 +1468,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha // Seasonal Immersion "ID": "EntoaroxSeasonalHouse | EntoaroxSeasonalBuildings | EntoaroxSeasonalImmersion | Entoarox.SeasonalImmersion", // changed in 1.1, 1.6 or earlier, and 1.7 // "UpdateKeys": [ "Chucklefish:4262" ], // Entoarox opted out of mod update checks - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.8.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -1487,7 +1489,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha // Send Items "ID": "Denifia.SendItems", "UpdateKeys": [ "Nexus:1087" ], // added in 1.0.3 (2017-10-04) - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0.2": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } @@ -1560,7 +1561,11 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { // Simple Sound Manager "ID": "Omegasis.SimpleSoundManager", - "UpdateKeys": [ "Nexus:1410" ] + "UpdateKeys": [ "Nexus:1410" ], + "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", + "Compatibility": { + "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 + } }, { // Simple Sprinklers @@ -1789,7 +1794,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha // Stone Bridge Over Pond (PondWithBridge) "ID": "PondWithBridge.dll", "UpdateKeys": [ "Nexus:316" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 }, @@ -1977,7 +1981,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha // Xnb Loader "ID": "Entoarox.XnbLoader", // "UpdateKeys": [ "Chucklefish:4506" ], // Entoarox opted out of mod update checks - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0.6": { "Status": "AssumeBroken" } // broke in SMAPI 2.0 } From c74b21141cfad4d02f899462e02e36818154e152 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 10 Oct 2017 00:49:54 -0400 Subject: [PATCH 166/186] work around race condition in game code --- src/SMAPI/Framework/SContentManager.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/SMAPI/Framework/SContentManager.cs b/src/SMAPI/Framework/SContentManager.cs index 43de6e96..f3a1dd9a 100644 --- a/src/SMAPI/Framework/SContentManager.cs +++ b/src/SMAPI/Framework/SContentManager.cs @@ -51,6 +51,9 @@ namespace StardewModdingAPI.Framework /// A lookup of the content managers which loaded each asset. private readonly IDictionary> AssetLoaders = new Dictionary>(); + /// An object locked to prevent concurrent changes to the underlying assets. + private readonly object Lock = new object(); + /********* ** Accessors @@ -128,7 +131,7 @@ namespace StardewModdingAPI.Framework /// The asset path relative to the loader root directory, not including the .xnb extension. public bool IsLoaded(string assetName) { - lock (this.Cache) + lock (this.Lock) { assetName = this.NormaliseAssetName(assetName); return this.IsNormalisedKeyLoaded(assetName); @@ -149,7 +152,7 @@ namespace StardewModdingAPI.Framework /// The content manager instance for which to load the asset. public T LoadFor(string assetName, ContentManager instance) { - lock (this.Cache) + lock (this.Lock) { assetName = this.NormaliseAssetName(assetName); @@ -192,7 +195,7 @@ namespace StardewModdingAPI.Framework /// The asset value. public void Inject(string assetName, T value) { - lock (this.Cache) + lock (this.Lock) { assetName = this.NormaliseAssetName(assetName); this.Cache[assetName] = value; @@ -209,7 +212,7 @@ namespace StardewModdingAPI.Framework /// Get the cached asset keys. public IEnumerable GetAssetKeys() { - lock (this.Cache) + lock (this.Lock) { IEnumerable GetAllAssetKeys() { @@ -260,7 +263,7 @@ namespace StardewModdingAPI.Framework /// Returns whether any cache entries were invalidated. public bool InvalidateCache(Func predicate, bool dispose = false) { - lock (this.Cache) + lock (this.Lock) { // find matching asset keys HashSet purgeCacheKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase); From 5dfb12c2781c73854dc9aff2ae12b0a8de83c408 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 10 Oct 2017 02:22:06 -0400 Subject: [PATCH 167/186] fix input events having decimal tile coordinates (#367) --- src/SMAPI/Framework/SGame.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 7287cab7..6f8f7cef 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -378,7 +378,7 @@ namespace StardewModdingAPI.Framework { // cursor position Vector2 screenPixels = new Vector2(Game1.getMouseX(), Game1.getMouseY()); - Vector2 tile = new Vector2((Game1.viewport.X + screenPixels.X) / Game1.tileSize, (Game1.viewport.Y + screenPixels.Y) / Game1.tileSize); + Vector2 tile = new Vector2((int)((Game1.viewport.X + screenPixels.X) / Game1.tileSize), (int)((Game1.viewport.Y + screenPixels.Y) / Game1.tileSize)); Vector2 grabTile = (Game1.mouseCursorTransparency > 0 && Utility.tileWithinRadiusOfPlayer((int)tile.X, (int)tile.Y, 1, Game1.player)) // derived from Game1.pressActionButton ? tile : Game1.player.GetGrabTile(); From 1c0d22e82c4690069754d211179d8aef636a3e7a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 10 Oct 2017 21:59:05 -0400 Subject: [PATCH 168/186] validate build context before build --- .../{ => BuildTasks}/DeployModTask.cs | 31 +------- .../BuildTasks/ValidateInstallTask.cs | 70 +++++++++++++++++++ .../StardewModdingAPI.ModBuildConfig.csproj | 3 +- src/SMAPI.ModBuildConfig/build/smapi.targets | 6 +- 4 files changed, 77 insertions(+), 33 deletions(-) rename src/SMAPI.ModBuildConfig/{ => BuildTasks}/DeployModTask.cs (76%) create mode 100644 src/SMAPI.ModBuildConfig/BuildTasks/ValidateInstallTask.cs diff --git a/src/SMAPI.ModBuildConfig/DeployModTask.cs b/src/SMAPI.ModBuildConfig/BuildTasks/DeployModTask.cs similarity index 76% rename from src/SMAPI.ModBuildConfig/DeployModTask.cs rename to src/SMAPI.ModBuildConfig/BuildTasks/DeployModTask.cs index 0bfe9b2d..433e602f 100644 --- a/src/SMAPI.ModBuildConfig/DeployModTask.cs +++ b/src/SMAPI.ModBuildConfig/BuildTasks/DeployModTask.cs @@ -6,26 +6,11 @@ using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using StardewModdingAPI.ModBuildConfig.Framework; -namespace StardewModdingAPI.ModBuildConfig +namespace StardewModdingAPI.ModBuildConfig.BuildTasks { /// A build task which deploys the mod files and prepares a release zip. public class DeployModTask : Task { - /********* - ** Properties - *********/ - /// The MSBuild platforms recognised by the build configuration. - private readonly HashSet ValidPlatforms = new HashSet(new[] { "OSX", "Unix", "Windows_NT" }, StringComparer.InvariantCultureIgnoreCase); - - /// The name of the game's main executable file. - private string GameExeName => this.Platform == "Windows_NT" - ? "Stardew Valley.exe" - : "StardewValley.exe"; - - /// The name of SMAPI's main executable file. - private readonly string SmapiExeName = "StardewModdingAPI.exe"; - - /********* ** Accessors *********/ @@ -49,10 +34,6 @@ namespace StardewModdingAPI.ModBuildConfig [Required] public string GameDir { get; set; } - /// The MSBuild OS value. - [Required] - public string Platform { get; set; } - /// Whether to enable copying the mod files into the game's Mods folder. [Required] public bool EnableModDeploy { get; set; } @@ -74,16 +55,6 @@ namespace StardewModdingAPI.ModBuildConfig try { - // validate context - if (!this.ValidPlatforms.Contains(this.Platform)) - throw new UserErrorException($"The mod build package doesn't recognise OS type '{this.Platform}'."); - if (!Directory.Exists(this.GameDir)) - throw new UserErrorException("The mod build package can't find your game path. See https://github.com/Pathoschild/SMAPI/blob/develop/docs/mod-build-config.md for help specifying it."); - if (!File.Exists(Path.Combine(this.GameDir, this.GameExeName))) - throw new UserErrorException($"The mod build package found a game folder at {this.GameDir}, but it doesn't contain the {this.GameExeName} file. If this folder is invalid, delete it and the package will autodetect another game install path."); - if (!File.Exists(Path.Combine(this.GameDir, this.SmapiExeName))) - throw new UserErrorException($"The mod build package found a game folder at {this.GameDir}, but it doesn't contain SMAPI. You need to install SMAPI before building the mod."); - // get mod info ModFileManager package = new ModFileManager(this.ProjectDir, this.TargetDir); diff --git a/src/SMAPI.ModBuildConfig/BuildTasks/ValidateInstallTask.cs b/src/SMAPI.ModBuildConfig/BuildTasks/ValidateInstallTask.cs new file mode 100644 index 00000000..2cc7dc9c --- /dev/null +++ b/src/SMAPI.ModBuildConfig/BuildTasks/ValidateInstallTask.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using StardewModdingAPI.ModBuildConfig.Framework; + +namespace StardewModdingAPI.ModBuildConfig.BuildTasks +{ + /// A build task which validates the install context. + public class ValidateInstallTask : Task + { + /********* + ** Properties + *********/ + /// The MSBuild platforms recognised by the build configuration. + private readonly HashSet ValidPlatforms = new HashSet(new[] { "OSX", "Unix", "Windows_NT" }, StringComparer.InvariantCultureIgnoreCase); + + /// The name of the game's main executable file. + private string GameExeName => this.Platform == "Windows_NT" + ? "Stardew Valley.exe" + : "StardewValley.exe"; + + /// The name of SMAPI's main executable file. + private readonly string SmapiExeName = "StardewModdingAPI.exe"; + + + /********* + ** Accessors + *********/ + /// The folder containing the game files. + public string GameDir { get; set; } + + /// The MSBuild OS value. + public string Platform { get; set; } + + + /********* + ** Public methods + *********/ + /// When overridden in a derived class, executes the task. + /// true if the task successfully executed; otherwise, false. + public override bool Execute() + { + try + { + if (!this.ValidPlatforms.Contains(this.Platform)) + throw new UserErrorException($"The mod build package doesn't recognise OS type '{this.Platform}'."); + if (!Directory.Exists(this.GameDir)) + throw new UserErrorException("The mod build package can't find your game path. See https://github.com/Pathoschild/SMAPI/blob/develop/docs/mod-build-config.md for help specifying it."); + if (!File.Exists(Path.Combine(this.GameDir, this.GameExeName))) + throw new UserErrorException($"The mod build package found a game folder at {this.GameDir}, but it doesn't contain the {this.GameExeName} file. If this folder is invalid, delete it and the package will autodetect another game install path."); + if (!File.Exists(Path.Combine(this.GameDir, this.SmapiExeName))) + throw new UserErrorException($"The mod build package found a game folder at {this.GameDir}, but it doesn't contain SMAPI. You need to install SMAPI before building the mod."); + + return true; + } + catch (UserErrorException ex) + { + this.Log.LogErrorFromException(ex); + return false; + } + catch (Exception ex) + { + this.Log.LogError($"The mod build package failed trying to deploy the mod.\n{ex}"); + return false; + } + } + } +} diff --git a/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj b/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj index e04f09a7..0007e53e 100644 --- a/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj +++ b/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj @@ -38,7 +38,8 @@ - + + diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets index 0010d8ff..9f3f13f5 100644 --- a/src/SMAPI.ModBuildConfig/build/smapi.targets +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -3,6 +3,7 @@ ** Import build tasks **********************************************--> + + + + From 1c7dfb519dd4238336a9a29d677219563e898dc7 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 10 Oct 2017 22:33:45 -0400 Subject: [PATCH 169/186] move validation back into .targets for MonoDevelop compatibility --- .../BuildTasks/ValidateInstallTask.cs | 70 ------------------- .../{BuildTasks => }/DeployModTask.cs | 2 +- .../StardewModdingAPI.ModBuildConfig.csproj | 3 +- src/SMAPI.ModBuildConfig/build/smapi.targets | 10 ++- 4 files changed, 11 insertions(+), 74 deletions(-) delete mode 100644 src/SMAPI.ModBuildConfig/BuildTasks/ValidateInstallTask.cs rename src/SMAPI.ModBuildConfig/{BuildTasks => }/DeployModTask.cs (99%) diff --git a/src/SMAPI.ModBuildConfig/BuildTasks/ValidateInstallTask.cs b/src/SMAPI.ModBuildConfig/BuildTasks/ValidateInstallTask.cs deleted file mode 100644 index 2cc7dc9c..00000000 --- a/src/SMAPI.ModBuildConfig/BuildTasks/ValidateInstallTask.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; -using StardewModdingAPI.ModBuildConfig.Framework; - -namespace StardewModdingAPI.ModBuildConfig.BuildTasks -{ - /// A build task which validates the install context. - public class ValidateInstallTask : Task - { - /********* - ** Properties - *********/ - /// The MSBuild platforms recognised by the build configuration. - private readonly HashSet ValidPlatforms = new HashSet(new[] { "OSX", "Unix", "Windows_NT" }, StringComparer.InvariantCultureIgnoreCase); - - /// The name of the game's main executable file. - private string GameExeName => this.Platform == "Windows_NT" - ? "Stardew Valley.exe" - : "StardewValley.exe"; - - /// The name of SMAPI's main executable file. - private readonly string SmapiExeName = "StardewModdingAPI.exe"; - - - /********* - ** Accessors - *********/ - /// The folder containing the game files. - public string GameDir { get; set; } - - /// The MSBuild OS value. - public string Platform { get; set; } - - - /********* - ** Public methods - *********/ - /// When overridden in a derived class, executes the task. - /// true if the task successfully executed; otherwise, false. - public override bool Execute() - { - try - { - if (!this.ValidPlatforms.Contains(this.Platform)) - throw new UserErrorException($"The mod build package doesn't recognise OS type '{this.Platform}'."); - if (!Directory.Exists(this.GameDir)) - throw new UserErrorException("The mod build package can't find your game path. See https://github.com/Pathoschild/SMAPI/blob/develop/docs/mod-build-config.md for help specifying it."); - if (!File.Exists(Path.Combine(this.GameDir, this.GameExeName))) - throw new UserErrorException($"The mod build package found a game folder at {this.GameDir}, but it doesn't contain the {this.GameExeName} file. If this folder is invalid, delete it and the package will autodetect another game install path."); - if (!File.Exists(Path.Combine(this.GameDir, this.SmapiExeName))) - throw new UserErrorException($"The mod build package found a game folder at {this.GameDir}, but it doesn't contain SMAPI. You need to install SMAPI before building the mod."); - - return true; - } - catch (UserErrorException ex) - { - this.Log.LogErrorFromException(ex); - return false; - } - catch (Exception ex) - { - this.Log.LogError($"The mod build package failed trying to deploy the mod.\n{ex}"); - return false; - } - } - } -} diff --git a/src/SMAPI.ModBuildConfig/BuildTasks/DeployModTask.cs b/src/SMAPI.ModBuildConfig/DeployModTask.cs similarity index 99% rename from src/SMAPI.ModBuildConfig/BuildTasks/DeployModTask.cs rename to src/SMAPI.ModBuildConfig/DeployModTask.cs index 433e602f..a09dd5d2 100644 --- a/src/SMAPI.ModBuildConfig/BuildTasks/DeployModTask.cs +++ b/src/SMAPI.ModBuildConfig/DeployModTask.cs @@ -6,7 +6,7 @@ using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using StardewModdingAPI.ModBuildConfig.Framework; -namespace StardewModdingAPI.ModBuildConfig.BuildTasks +namespace StardewModdingAPI.ModBuildConfig { /// A build task which deploys the mod files and prepares a release zip. public class DeployModTask : Task diff --git a/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj b/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj index 0007e53e..e04f09a7 100644 --- a/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj +++ b/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj @@ -38,8 +38,7 @@ - - + diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets index 9f3f13f5..f7e75e23 100644 --- a/src/SMAPI.ModBuildConfig/build/smapi.targets +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -118,9 +118,17 @@ + - + + + + + + + + Date: Tue, 10 Oct 2017 22:48:06 -0400 Subject: [PATCH 170/186] bump mod build package version for release --- src/SMAPI.ModBuildConfig/Properties/AssemblyInfo.cs | 4 ++-- src/SMAPI.ModBuildConfig/package.nuspec | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/SMAPI.ModBuildConfig/Properties/AssemblyInfo.cs b/src/SMAPI.ModBuildConfig/Properties/AssemblyInfo.cs index 57dde638..19d2517b 100644 --- a/src/SMAPI.ModBuildConfig/Properties/AssemblyInfo.cs +++ b/src/SMAPI.ModBuildConfig/Properties/AssemblyInfo.cs @@ -5,5 +5,5 @@ using System.Runtime.InteropServices; [assembly: AssemblyDescription("")] [assembly: Guid("ea4f1e80-743f-4a1d-9757-ae66904a196a")] [assembly: ComVisible(false)] -[assembly: AssemblyVersion("1.7.1.0")] -[assembly: AssemblyFileVersion("1.7.1.0")] +[assembly: AssemblyVersion("2.0.0.0")] +[assembly: AssemblyFileVersion("2.0.0.0")] diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec index 3ab5db8e..36d9582a 100644 --- a/src/SMAPI.ModBuildConfig/package.nuspec +++ b/src/SMAPI.ModBuildConfig/package.nuspec @@ -2,7 +2,7 @@ Pathoschild.Stardew.ModBuildConfig - 2.0-beta + 2.0.0 Build package for SMAPI mods Pathoschild Pathoschild From 127b36dedd9defe11b2d3f9ce0fa088efcf8c5f0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 11 Oct 2017 15:30:37 -0400 Subject: [PATCH 171/186] rm artifact --- src/SMAPI.ModBuildConfig/build/smapi.targets | 1 - 1 file changed, 1 deletion(-) diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets index f7e75e23..c0319e22 100644 --- a/src/SMAPI.ModBuildConfig/build/smapi.targets +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -3,7 +3,6 @@ ** Import build tasks **********************************************--> - @@ -30,6 +30,7 @@ For mod developers: * Removed support for mods with a non-unique `UniqueID` value in their manifest. * Removed access to SMAPI internals through the reflection helper, to discourage fragile mods. * Fixed `TimeEvents.AfterDayStarted` being raised during the new-game intro. +* Fixed SMAPI allowing map tilesheets with absolute or directory-climbing paths. These are now rejected even if the path exists, to avoid problems when players install the mod. For power users: * Added command-line arguments to the SMAPI installer so it can be scripted. diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs index 4440ae40..4f5bd2f0 100644 --- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs @@ -239,6 +239,10 @@ namespace StardewModdingAPI.Framework.ModHelpers { string imageSource = tilesheet.ImageSource; + // validate + if (Path.IsPathRooted(imageSource) || imageSource.Split(SContentManager.PossiblePathSeparators).Contains("..")) + throw new ContentLoadException($"The '{imageSource}' tilesheet couldn't be loaded. Tilesheet paths must be a relative path without directory climbing (../)."); + // get seasonal name (if applicable) string seasonalImageSource = null; if (Game1.currentSeason != null) diff --git a/src/SMAPI/Framework/SContentManager.cs b/src/SMAPI/Framework/SContentManager.cs index f3a1dd9a..db202567 100644 --- a/src/SMAPI/Framework/SContentManager.cs +++ b/src/SMAPI/Framework/SContentManager.cs @@ -21,9 +21,6 @@ namespace StardewModdingAPI.Framework /********* ** Properties *********/ - /// The possible directory separator characters in an asset key. - private static readonly char[] PossiblePathSeparators = new[] { '/', '\\', Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }.Distinct().ToArray(); - /// The preferred directory separator chaeacter in an asset key. private static readonly string PreferredPathSeparator = Path.DirectorySeparatorChar.ToString(); @@ -64,8 +61,11 @@ namespace StardewModdingAPI.Framework /// Interceptors which edit matching assets after they're loaded. internal IDictionary> Editors { get; } = new Dictionary>(); + /// The possible directory separator characters in an asset key. + internal static readonly char[] PossiblePathSeparators = new[] { '/', '\\', Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }.Distinct().ToArray(); + /// The absolute path to the . - public string FullRootDirectory => Path.Combine(Constants.ExecutionPath, this.RootDirectory); + internal string FullRootDirectory => Path.Combine(Constants.ExecutionPath, this.RootDirectory); /********* From b3ac93a0dcc3ff40d3fed0e1ad293619f9522524 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 12 Oct 2017 22:20:56 -0400 Subject: [PATCH 176/186] bump version for 2.0-beta.2 --- src/SMAPI/Constants.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 4d0a9ca9..23cb67f9 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -29,7 +29,7 @@ namespace StardewModdingAPI ** Public ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } = new SemanticVersion(2, 0, 0, "beta.1"); + public static ISemanticVersion ApiVersion { get; } = new SemanticVersion(2, 0, 0, "beta.2"); /// The minimum supported version of Stardew Valley. public static ISemanticVersion MinimumGameVersion { get; } = new SemanticVersion("1.2.30"); From aa5c5b27839b72343f751ffabd6c4625d1fc3f80 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 13 Oct 2017 00:33:05 -0400 Subject: [PATCH 177/186] fix SDate.Now() crashing during new-game intro (#369) --- docs/release-notes.md | 1 + src/SMAPI.Tests/Utilities/SDateTests.cs | 2 + src/SMAPI/Utilities/SDate.cs | 56 +++++++++++++++++-------- 3 files changed, 41 insertions(+), 18 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index fd59bd07..64588744 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -29,6 +29,7 @@ For mod developers: * Removed support for mods with no `Name`, `Version`, or `UniqueID` in their manifest. * Removed support for mods with a non-unique `UniqueID` value in their manifest. * Removed access to SMAPI internals through the reflection helper, to discourage fragile mods. +* Fixed `SDate.Now()` crashing when called during the new-game intro. * Fixed `TimeEvents.AfterDayStarted` being raised during the new-game intro. * Fixed SMAPI allowing map tilesheets with absolute or directory-climbing paths. These are now rejected even if the path exists, to avoid problems when players install the mod. diff --git a/src/SMAPI.Tests/Utilities/SDateTests.cs b/src/SMAPI.Tests/Utilities/SDateTests.cs index 25acbaf3..86a0d3d0 100644 --- a/src/SMAPI.Tests/Utilities/SDateTests.cs +++ b/src/SMAPI.Tests/Utilities/SDateTests.cs @@ -69,6 +69,8 @@ namespace StardewModdingAPI.Tests.Utilities [TestCase(01, "Spring", 1)] // seasons are case-sensitive [TestCase(01, "springs", 1)] // invalid season name [TestCase(-1, "spring", 1)] // day < 0 + [TestCase(0, "spring", 1)] // day zero + [TestCase(0, "spring", 2)] // day zero [TestCase(29, "spring", 1)] // day > 28 [TestCase(01, "spring", -1)] // year < 1 [TestCase(01, "spring", 0)] // year < 1 diff --git a/src/SMAPI/Utilities/SDate.cs b/src/SMAPI/Utilities/SDate.cs index 326d7fc7..2630731a 100644 --- a/src/SMAPI/Utilities/SDate.cs +++ b/src/SMAPI/Utilities/SDate.cs @@ -52,28 +52,12 @@ namespace StardewModdingAPI.Utilities /// The year. /// One of the arguments has an invalid value (like day 35). public SDate(int day, string season, int year) - { - // validate - if (season == null) - throw new ArgumentNullException(nameof(season)); - if (!this.Seasons.Contains(season)) - throw new ArgumentException($"Unknown season '{season}', must be one of [{string.Join(", ", this.Seasons)}]."); - if (day < 1 || day > this.DaysInSeason) - throw new ArgumentException($"Invalid day '{day}', must be a value from 1 to {this.DaysInSeason}."); - if (year < 1) - throw new ArgumentException($"Invalid year '{year}', must be at least 1."); - - // initialise - this.Day = day; - this.Season = season; - this.Year = year; - this.DayOfWeek = this.GetDayOfWeek(); - } + : this(day, season, year, allowDayZero: false) { } /// Get the current in-game date. public static SDate Now() { - return new SDate(Game1.dayOfMonth, Game1.currentSeason, Game1.year); + return new SDate(Game1.dayOfMonth, Game1.currentSeason, Game1.year, allowDayZero: true); } /// Get a new date with the given number of days added. @@ -195,6 +179,42 @@ namespace StardewModdingAPI.Utilities /********* ** Private methods *********/ + /// Construct an instance. + /// The day of month. + /// The season name. + /// The year. + /// Whether to allow 0 spring Y1 as a valid date. + /// One of the arguments has an invalid value (like day 35). + private SDate(int day, string season, int year, bool allowDayZero) + { + // validate + if (season == null) + throw new ArgumentNullException(nameof(season)); + if (!this.Seasons.Contains(season)) + throw new ArgumentException($"Unknown season '{season}', must be one of [{string.Join(", ", this.Seasons)}]."); + if (day < 0 || day > this.DaysInSeason) + throw new ArgumentException($"Invalid day '{day}', must be a value from 1 to {this.DaysInSeason}."); + if(day == 0 && !(allowDayZero && this.IsDayZero(day, season, year))) + throw new ArgumentException($"Invalid day '{day}', must be a value from 1 to {this.DaysInSeason}."); + if (year < 1) + throw new ArgumentException($"Invalid year '{year}', must be at least 1."); + + // initialise + this.Day = day; + this.Season = season; + this.Year = year; + this.DayOfWeek = this.GetDayOfWeek(); + } + + /// Get whether a date represents 0 spring Y1, which is the date during the in-game intro. + /// The day of month. + /// The season name. + /// The year. + private bool IsDayZero(int day, string season, int year) + { + return day == 0 && season == "spring" && year == 1; + } + /// Get the day of week for the current date. private DayOfWeek GetDayOfWeek() { From f04a68697ae64058540b71639efbb761d55a7b58 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 13 Oct 2017 12:58:18 -0400 Subject: [PATCH 178/186] update unit test dependencies --- src/SMAPI.Tests/StardewModdingAPI.Tests.csproj | 11 ++++++----- src/SMAPI.Tests/packages.config | 6 +++--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj b/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj index 42c3318f..c3823cdb 100644 --- a/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj +++ b/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj @@ -1,4 +1,4 @@ - + @@ -30,11 +30,11 @@ 4 - - ..\packages\Castle.Core.4.1.1\lib\net45\Castle.Core.dll + + ..\packages\Castle.Core.4.2.1\lib\net45\Castle.Core.dll - - ..\packages\Moq.4.7.99\lib\net45\Moq.dll + + ..\packages\Moq.4.7.142\lib\net45\Moq.dll ..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll @@ -43,6 +43,7 @@ ..\packages\NUnit.3.8.1\lib\net45\nunit.framework.dll + diff --git a/src/SMAPI.Tests/packages.config b/src/SMAPI.Tests/packages.config index 5fdfebdb..97fb2801 100644 --- a/src/SMAPI.Tests/packages.config +++ b/src/SMAPI.Tests/packages.config @@ -1,7 +1,7 @@  - - + + - + \ No newline at end of file From 0fa552e2b98db9e665e59a1eb0508151bce4e57f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 13 Oct 2017 13:11:10 -0400 Subject: [PATCH 179/186] update test launch URL for web API --- src/SMAPI.Web/Properties/launchSettings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SMAPI.Web/Properties/launchSettings.json b/src/SMAPI.Web/Properties/launchSettings.json index 3acee14d..a0760365 100644 --- a/src/SMAPI.Web/Properties/launchSettings.json +++ b/src/SMAPI.Web/Properties/launchSettings.json @@ -11,7 +11,7 @@ "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, - "launchUrl": "api/v1.0/mods", + "launchUrl": "api/1.0/mods?modKeys=nexus:541,chucklefish:4228,github:Zoryn4163/SMAPI-Mods", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } @@ -19,7 +19,7 @@ "Dewdrop": { "commandName": "Project", "launchBrowser": true, - "launchUrl": "api/v1.0/mods", + "launchUrl": "api/1.0/mods?modKeys=nexus:541,chucklefish:4228,github:Zoryn4163/SMAPI-Mods", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, From 42d1024e04ad40d6441c5c4582239505063038dd Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 13 Oct 2017 13:14:24 -0400 Subject: [PATCH 180/186] update HTML agility pack --- src/SMAPI.Web/StardewModdingAPI.Web.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SMAPI.Web/StardewModdingAPI.Web.csproj b/src/SMAPI.Web/StardewModdingAPI.Web.csproj index ec1311f4..b5b0ff07 100644 --- a/src/SMAPI.Web/StardewModdingAPI.Web.csproj +++ b/src/SMAPI.Web/StardewModdingAPI.Web.csproj @@ -10,7 +10,7 @@ - + From ff718d7993793ef4e93d49dc82f5ce3bb7de208e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 13 Oct 2017 13:17:58 -0400 Subject: [PATCH 181/186] update Json.NET --- docs/release-notes.md | 1 + src/SMAPI.Tests/StardewModdingAPI.Tests.csproj | 4 ++-- src/SMAPI.Tests/packages.config | 2 +- src/SMAPI/StardewModdingAPI.csproj | 7 +++---- src/SMAPI/packages.config | 2 +- src/TrainerMod/TrainerMod.csproj | 7 +++---- src/TrainerMod/packages.config | 2 +- 7 files changed, 12 insertions(+), 13 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 64588744..39662784 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -24,6 +24,7 @@ For mod developers: * Added support for specifying the mod version as a string (like `"1.0-alpha"`) in `manifest.json`. * Added day of week to `SDate` instances. * Added `IEquatable` to `ISemanticVersion`. +* Updated Json.NET from 8.0.3 to 10.0.3. * Removed the TrainerMod's `save` and `load` commands. * Removed all deprecated code. * Removed support for mods with no `Name`, `Version`, or `UniqueID` in their manifest. diff --git a/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj b/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj index c3823cdb..c7a67306 100644 --- a/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj +++ b/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj @@ -36,8 +36,8 @@ ..\packages\Moq.4.7.142\lib\net45\Moq.dll - - ..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll + + ..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll ..\packages\NUnit.3.8.1\lib\net45\nunit.framework.dll diff --git a/src/SMAPI.Tests/packages.config b/src/SMAPI.Tests/packages.config index 97fb2801..7a91e807 100644 --- a/src/SMAPI.Tests/packages.config +++ b/src/SMAPI.Tests/packages.config @@ -2,6 +2,6 @@ - + \ No newline at end of file diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 35a784cd..6bd803c3 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -1,4 +1,4 @@ - + @@ -65,9 +65,8 @@ ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Pdb.dll True - - ..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll - True + + ..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll diff --git a/src/SMAPI/packages.config b/src/SMAPI/packages.config index e5fa3c3a..98d742c7 100644 --- a/src/SMAPI/packages.config +++ b/src/SMAPI/packages.config @@ -1,5 +1,5 @@  - + \ No newline at end of file diff --git a/src/TrainerMod/TrainerMod.csproj b/src/TrainerMod/TrainerMod.csproj index 3182338c..cb5ec47e 100644 --- a/src/TrainerMod/TrainerMod.csproj +++ b/src/TrainerMod/TrainerMod.csproj @@ -1,4 +1,4 @@ - + @@ -36,9 +36,8 @@ x86 - - ..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll - False + + ..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll diff --git a/src/TrainerMod/packages.config b/src/TrainerMod/packages.config index 75e68e71..ee51c237 100644 --- a/src/TrainerMod/packages.config +++ b/src/TrainerMod/packages.config @@ -1,4 +1,4 @@  - + \ No newline at end of file From f663ed3359d89448e03022d4caa6de662f0ab077 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 13 Oct 2017 23:13:06 -0400 Subject: [PATCH 182/186] fix assets not being reloaded when mods implement IAssetEditor or IAssetLoader directly --- src/SMAPI/Program.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 293c9da7..fe306e24 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -774,6 +774,13 @@ namespace StardewModdingAPI // add interceptors if (metadata.Mod.Helper.Content is ContentHelper helper) { + // ReSharper disable SuspiciousTypeConversion.Global + if (metadata.Mod is IAssetEditor editor) + helper.ObservableAssetEditors.Add(editor); + if (metadata.Mod is IAssetLoader loader) + helper.ObservableAssetLoaders.Add(loader); + // ReSharper restore SuspiciousTypeConversion.Global + this.ContentManager.Editors[metadata] = helper.ObservableAssetEditors; this.ContentManager.Loaders[metadata] = helper.ObservableAssetLoaders; } From c5932233eb8e9bce26c5cea42d1afcdb7e5e4a85 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 13 Oct 2017 23:13:31 -0400 Subject: [PATCH 183/186] update mod data --- src/SMAPI/StardewModdingAPI.config.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/SMAPI/StardewModdingAPI.config.json b/src/SMAPI/StardewModdingAPI.config.json index 473fe54a..fa3bdcc9 100644 --- a/src/SMAPI/StardewModdingAPI.config.json +++ b/src/SMAPI/StardewModdingAPI.config.json @@ -1859,7 +1859,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha // Three-heart Dance Partner "ID": "ThreeHeartDancePartner", "UpdateKeys": [ "Nexus:500" ], - "AlternativeUrl": "http://stardewvalleywiki.com/Modding:SMAPI_2.0", "Compatibility": { "~1.0.1": { "Status": "AssumeBroken" } // broke in SDV 1.2 } From b71601a2520938b1a09d635007dba26854fd845a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 13 Oct 2017 23:26:27 -0400 Subject: [PATCH 184/186] fix recipe data not being reloaded when needed --- src/SMAPI/Metadata/CoreAssets.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/SMAPI/Metadata/CoreAssets.cs b/src/SMAPI/Metadata/CoreAssets.cs index 24f23af7..5a98da4b 100644 --- a/src/SMAPI/Metadata/CoreAssets.cs +++ b/src/SMAPI/Metadata/CoreAssets.cs @@ -37,6 +37,10 @@ namespace StardewModdingAPI.Metadata this.SingletonSetters = new Dictionary> { + // from CraftingRecipe.InitShared + ["Data\\CraftingRecipes"] = (content, key) => CraftingRecipe.craftingRecipes = content.Load>(key), + ["Data\\CookingRecipes"] = (content, key) => CraftingRecipe.cookingRecipes = content.Load>(key), + // from Game1.loadContent ["LooseSprites\\daybg"] = (content, key) => Game1.daybg = content.Load(key), ["LooseSprites\\nightbg"] = (content, key) => Game1.nightbg = content.Load(key), From 11b889992cd4d29099116e08e5560e040edd4506 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 13 Oct 2017 23:29:24 -0400 Subject: [PATCH 185/186] move SButtons into root --- src/SMAPI/{Utilities => }/SButton.cs | 2 +- src/SMAPI/StardewModdingAPI.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/SMAPI/{Utilities => }/SButton.cs (99%) diff --git a/src/SMAPI/Utilities/SButton.cs b/src/SMAPI/SButton.cs similarity index 99% rename from src/SMAPI/Utilities/SButton.cs rename to src/SMAPI/SButton.cs index fa5ae648..0ec799db 100644 --- a/src/SMAPI/Utilities/SButton.cs +++ b/src/SMAPI/SButton.cs @@ -2,7 +2,7 @@ using System; using Microsoft.Xna.Framework.Input; using StardewValley; -namespace StardewModdingAPI.Utilities +namespace StardewModdingAPI { /// A unified button constant which includes all controller, keyboard, and mouse buttons. /// Derived from , , and . diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 6bd803c3..b8d5990e 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -229,7 +229,7 @@ - + From 8aec1eff99858716afe7b8604b512181f78c214f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 14 Oct 2017 11:39:52 -0400 Subject: [PATCH 186/186] update for 2.0 release --- docs/release-notes.md | 63 ++++++++++++++++++++++++++++++------ src/SMAPI/Constants.cs | 2 +- src/TrainerMod/manifest.json | 6 ++-- 3 files changed, 57 insertions(+), 14 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 39662784..99e771ce 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,7 +1,54 @@ # Release notes -## 2.0 (upcoming) - +## 2.0 +### Release highlights +* **Mod update checks** + SMAPI now checks if your mods have updates available, and will alert you in the console with a convenient link to the + mod page. This works with mods from the Chucklefish mod site, GitHub, or Nexus Mods. SMAPI 2.0 launches with + update-check support for over 250 existing mods, and more will be added as modders enable the feature. +* **Mod stability warnings** + SMAPI now detects when a mod contains code which can destabilise your game or corrupt your save, and shows a warning + in the console. + +* **Simpler console** + The console is now simpler and easier to read, some commands have been streamlined, and the colors now adjust to fit + your terminal background color. + +* **New features for modders** + SMAPI 2.0 adds several features to enable new kinds of mods (see + [API documentation](https://stardewvalleywiki.com/Modding:SMAPI_APIs)). + + The **content API** lets you edit, inject, and reload XNB data loaded by the game at any time. This let SMAPI mods do + anything previously only possible with XNB mods, and enables new mod scenarios not possible with XNB mods (e.g. + seasonal textures, NPC clothing that depend on the weather or location, etc). + + The **input events** unify controller + keyboard + mouse input into one event and constant for easy handling, and add + metadata like the cursor position and grab tile to support click handling. They also let you prevent the game from + receiving input, to enable new scenarios like action highjacking and UI overlays. + + The mod manifest has a few changes too: + * The **`UpdateKeys` field** lets you specify your Chucklefish, GitHub, or Nexus mod IDs. SMAPI will automatically + check for newer versions and notify the player. + * The **version field** is now a semantic string like `"1.0-alpha"`. (Mods which still use the version structure will + still work fine.) + * The **dependencies field** now lets you add optional dependencies which should be loaded first if available. + + Finally, the `SDate` utility now has a `DayOfWeek` field for more convenient date calculations, and `ISemanticVersion` + now implements `IEquatable`. + +* **Goodbye deprecated code** + SMAPI 2.0 removes all deprecated code to unshackle future development. That includes... + * removed all code marked obsolete; + * removed TrainerMod's `save` and `load` commands; + * removed support for mods with no `Name`, `Version`, or `UniqueID` in their manifest; + * removed support for multiple mods having the same `UniqueID` value; + * removed access to SMAPI internals through the reflection helper. + +* **Command-line install** + For power users and mod managers, the SMAPI installer can now be scripted using command-line arguments + (see [technical docs](technical-docs.md#command-line-arguments)). + +### Change log For players: * SMAPI now alerts you when mods have new versions available. * SMAPI now warns you about mods which may impact game stability or compatibility. @@ -12,14 +59,10 @@ For players: * Fixed collection-changed errors during startup for some players. For mod developers: -* Added support for editing, injecting, and reloading XNB data loaded by the game at any time. - _This let SMAPI mods do anything previously only possible with XNB mods, and enables new mod scenarios not possible with XNB mods (e.g. seasonal textures, NPC clothing that depend on the weather or location, etc)._ -* Added support for automatic mod update checks. - _Add your Chucklefish, GitHub, or Nexus mod ID to the manifest and SMAPI will check for updates automatically. SMAPI will retroactively enable updates for most existing mods._ -* Added unified input events. - _The new `InputEvents` combine keyboard + mouse + controller input into one event and constant for easy handling, add metadata like the cursor position and grab tile to support click handling._ -* Added support for suppressing input. - _You can prevent the game from receiving input via `InputEvents`, to enable new scenarios like action highjacking and UI overlays._ +* Added support for editing, injecting, and reloading XNB data loaded by the game at any time. +* Added support for automatic mod update checks. +* Added unified input events. +* Added support for suppressing input. * Added support for optional dependencies. * Added support for specifying the mod version as a string (like `"1.0-alpha"`) in `manifest.json`. * Added day of week to `SDate` instances. diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 23cb67f9..7721fd5e 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -29,7 +29,7 @@ namespace StardewModdingAPI ** Public ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } = new SemanticVersion(2, 0, 0, "beta.2"); + public static ISemanticVersion ApiVersion { get; } = new SemanticVersion(2, 0, 0); /// The minimum supported version of Stardew Valley. public static ISemanticVersion MinimumGameVersion { get; } = new SemanticVersion("1.2.30"); diff --git a/src/TrainerMod/manifest.json b/src/TrainerMod/manifest.json index 20b40f8a..22e35bce 100644 --- a/src/TrainerMod/manifest.json +++ b/src/TrainerMod/manifest.json @@ -2,9 +2,9 @@ "Name": "Trainer Mod", "Author": "SMAPI", "Version": { - "MajorVersion": 1, - "MinorVersion": 15, - "PatchVersion": 4, + "MajorVersion": 2, + "MinorVersion": 0, + "PatchVersion": 0, "Build": null }, "Description": "Adds SMAPI console commands that let you manipulate the game.",