diff --git a/.editorconfig b/.editorconfig
index 126fdbd4..5bfc44bd 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -3,7 +3,7 @@ root: true
##########
## General formatting
-## documentation: http://editorconfig.org
+## documentation: https://editorconfig.org
##########
[*]
indent_style = space
diff --git a/build/GlobalAssemblyInfo.cs b/build/GlobalAssemblyInfo.cs
index 2e4f9373..c022afda 100644
--- a/build/GlobalAssemblyInfo.cs
+++ b/build/GlobalAssemblyInfo.cs
@@ -1,5 +1,5 @@
using System.Reflection;
[assembly: AssemblyProduct("SMAPI")]
-[assembly: AssemblyVersion("2.7.0")]
-[assembly: AssemblyFileVersion("2.7.0")]
+[assembly: AssemblyVersion("2.8.1")]
+[assembly: AssemblyFileVersion("2.8.1")]
diff --git a/build/common.targets b/build/common.targets
index 5b6511f8..d9ad89f4 100644
--- a/build/common.targets
+++ b/build/common.targets
@@ -9,13 +9,20 @@
$(HOME)/GOG Games/Stardew Valley/game$(HOME)/.local/share/Steam/steamapps/common/Stardew Valley$(HOME)/.steam/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\GalaxyClient\Games\Stardew Valley
+ C:\Program Files\GOG Galaxy\Games\Stardew Valley
+ C:\Program Files\Steam\steamapps\common\Stardew Valley
+
C:\Program Files (x86)\GalaxyClient\Games\Stardew ValleyC:\Program Files (x86)\GOG Galaxy\Games\Stardew ValleyC:\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))
@@ -31,60 +38,57 @@
-
-
-
-
-
- False
-
-
- False
-
-
- False
-
-
- False
-
-
-
-
- $(GamePath)\Netcode.dll
- False
-
-
- $(GamePath)\Stardew Valley.exe
- False
-
-
- $(GamePath)\xTile.dll
- False
- False
-
-
-
-
-
-
-
- $(GamePath)\MonoGame.Framework.dll
- False
- False
-
-
-
-
- $(GamePath)\StardewValley.exe
- False
-
-
- $(GamePath)\xTile.dll
- False
-
-
-
-
+
+
+
+ $(GamePath)\Stardew Valley.exe
+ False
+
+
+ $(GamePath)\Netcode.dll
+ False
+
+
+ False
+
+
+ False
+
+
+ False
+
+
+ False
+
+
+
+
+
+
+ $(GamePath)\StardewValley.exe
+ False
+
+
+ $(GamePath)\MonoGame.Framework.dll
+ False
+
+
+
+
+
+
+ $(GamePath)\GalaxyCSharp.dll
+ False
+
+
+ $(GamePath)\Lidgren.Network.dll
+ False
+
+
+ $(GamePath)\xTile.dll
+ False
+
+
@@ -99,14 +103,13 @@
-
-
-
-
-
-
+
+
+
+
+
@@ -114,12 +117,14 @@
-
-
+
+
+
-
-
+
+
+
diff --git a/build/prepare-install-package.targets b/build/prepare-install-package.targets
index 79185896..aeed101d 100644
--- a/build/prepare-install-package.targets
+++ b/build/prepare-install-package.targets
@@ -10,60 +10,113 @@
$(SolutionDir)\..\bin\$(Configuration)$(CompiledRootPath)\SMAPI$(CompiledRootPath)\SMAPI.Toolkit\net4.5
- $(SolutionDir)\..\bin\Packaged
- $(PackagePath)\internal
+ $(SolutionDir)\..\bin\SMAPI installer
+ $(SolutionDir)\..\bin\SMAPI installer for developers
+ windows
+ unix
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/build/prepare-nuget-package.targets b/build/prepare-nuget-package.targets
index 11d1845e..0b4320a7 100644
--- a/build/prepare-nuget-package.targets
+++ b/build/prepare-nuget-package.targets
@@ -12,11 +12,12 @@
-
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/docs/README.md b/docs/README.md
index e7d6d682..b8e3b50b 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1,4 +1,4 @@
-**SMAPI** is an open-source modding API for [Stardew Valley](http://stardewvalley.net/) that lets
+**SMAPI** is an open-source modding API for [Stardew Valley](https://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 eight main purposes:
diff --git a/docs/release-notes.md b/docs/release-notes.md
index 133006e8..76bf4b15 100644
--- a/docs/release-notes.md
+++ b/docs/release-notes.md
@@ -1,4 +1,74 @@
# Release notes
+## 2.8.1
+* Fixed installer error on Windows.
+
+## 2.8
+* For players:
+ * Reorganised SMAPI files:
+ * Moved most SMAPI files into a `smapi-internal` subfolder (so your game folder is less messy).
+ * Moved save backups into a `save-backups` subfolder (so they're easier to find).
+ * Simplified the installer files to avoid confusion.
+ * Added support for organising mods into subfolders.
+ * Added support for [ignoring mod folders](https://stardewvalleywiki.com/Modding:Player_Guide/Getting_Started#Install_mods).
+ * Update checks now work even for mods without update keys in most cases.
+ * SMAPI now prevents a crash caused by mods adding dialogue the game can't parse.
+ * SMAPI now recommends a compatible SMAPI version if you have an older game version.
+ * Improved various error messages to be more clear and intuitive.
+ * Improved compatibility with various Linux shells (thanks to lqdev!), and prefer xterm when available.
+ * Fixed transparency issues on Linux/Mac for some mod images.
+ * Fixed error when a mod manifest is corrupted.
+ * Fixed error when a mod adds an unnamed location.
+ * Fixed friendly error no longer shown when SMAPI isn't run from the game folder.
+ * Fixed some Windows install paths not detected.
+ * Fixed installer duplicating bundled mods if you moved them after the last install.
+ * Fixed installer allowing custom mods to be bundled with the install.
+ * Fixed some translation issues not shown as warnings.
+ * Fixed dependencies not correctly enforced if the dependency is installed but failed to load.
+ * Fixed some errors logged as SMAPI instead of the affected mod.
+ * Fixed crash log deleted immediately when you relaunch the game.
+ * Updated compatibility list.
+
+* For the web UI:
+ * Added a [mod compatibility page](https://mods.smapi.io) and [privacy page](https://smapi.io/privacy).
+ * The log parser now has a separate filter for game messages.
+ * The log parser now shows content pack authors (thanks to danvolchek!).
+ * Tweaked log parser UI (thanks to danvolchek!).
+ * Fixed log parser instructions for Mac.
+
+* For modders:
+ * Added [data API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Data) to store mod data in the save file or app data.
+ * Added [multiplayer API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Multiplayer) and [events](https://stardewvalleywiki.com/Modding:Modder_Guide/Apis/Events#Multiplayer_2) to send/receive messages and get connected player info.
+ * Added [verbose logging](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Logging#Verbose_logging) feature.
+ * Added `IContentPack.WriteJsonFile` method.
+ * Added IntelliSense documentation for the non-developers version of SMAPI.
+ * Added more events to the prototype `helper.Events` for SMAPI 3.0.
+ * Added `SkillType` enum constant.
+ * Improved content API:
+ * added support for overlaying image assets with semi-transparency;
+ * mods can now load PNGs even if the game is currently drawing.
+ * When comparing mod versions, SMAPI now assigns the lowest precedence to `-unofficial` (e.g. `1.0-beta > 1.0-unofficial`).
+ * Fixed content packs' `ReadJsonFile` allowing non-relative paths.
+ * Fixed content packs always failing to load if they declare a dependency on a SMAPI mod.
+ * Fixed trace logs not showing path for invalid mods.
+ * Fixed 'no update keys' warning not shown for mods with only invalid update keys.
+ * Fixed `Context.IsPlayerFree` being true while the player is mid-warp in multiplayer.
+ * Fixed update-check errors sometimes being overwritten with a generic message.
+ * Suppressed the game's 'added crickets' debug output.
+ * Updated dependencies (Harmony 1.0.9.1 → 1.2.0.1, Mono.Cecil 0.10 → 0.10.1).
+ * **Deprecations:**
+ * Non-string manifest versions are now deprecated and will stop working in SMAPI 3.0. Affected mods should use a string version, like `"Version": "1.0.0"`.
+ * `ISemanticVersion.Build` is now deprecated and will be removed in SMAPI 3.0. Affected mods should use `ISemanticVersion.PrereleaseTag` instead.
+ * **Breaking changes:**
+ * `helper.ModRegistry` now returns `IModInfo` instead of `IManifest` directly. This lets SMAPI return more metadata about mods. This doesn't affect any mods that didn't already break in Stardew Valley 1.3.32.
+ * Most SMAPI files have been moved into a `smapi-internal` subfolder. This won't affect compiled mod releases, but you'll need to update the build config NuGet package.
+
+* For SMAPI developers:
+ * Added support for parallel stable/beta unofficial updates in update checks.
+ * Added a 'paranoid warnings' option which reports mods using potentially sensitive .NET APIs (like file or shell access) in the mod issues list.
+ * Adjusted `SaveBackup` mod to make it easier to account for custom mod subfolders in the installer.
+ * Installer no longer special-cases Omegasis' older `SaveBackup` mod (now named `AdvancedSaveBackup`).
+ * Fixed mod web API returning a concatenated name for mods with alternate names.
+
## 2.7
* For players:
* Updated for Stardew Valley 1.3.28.
@@ -11,6 +81,7 @@
* Fixed `player_add` command not recognising return scepter.
* Fixed `player_add` command showing fish twice.
* Fixed some SMAPI logs not deleted when starting a new session.
+ * Updated compatibility list.
* For modders:
* Added support for `.json` data files in the content API (including Content Patcher).
@@ -22,7 +93,6 @@
* All enums are now JSON-serialised by name instead of numeric value. (Previously only a few enums were serialised that way. JSON files which already have numeric enum values will still be parsed fine.)
* Fixed false compatibility error when constructing multidimensional arrays.
* Fixed `.ToSButton()` methods not being public.
- * Updated compatibility list.
* For SMAPI developers:
* Dropped support for pre-SMAPI-2.6 update checks in the web API.
@@ -609,7 +679,7 @@ For mod developers:
* The SMAPI log now always uses `\r\n` line endings to simplify crossplatform viewing.
* Fixed `SaveEvents.AfterLoad` being raised during the new-game intro before the player is initialised.
* Fixed SMAPI not recognising `Mod` instances that don't subclass `Mod` directly.
-* Several obsolete APIs have been removed (see [deprecation guide](http://canimod.com/guides/updating-a-smapi-mod)),
+* Several obsolete APIs have been removed (see [migration guides](https://stardewvalleywiki.com/Modding:Index#Migration_guides)),
and all _notice_-level deprecations have been increased to _info_.
* Removed the experimental `IConfigFile`.
@@ -692,7 +762,7 @@ For players:
For developers:
* Deprecated `Version` in favour of `SemanticVersion`.
- _This new implementation is [semver 2.0](http://semver.org/)-compliant, introduces `NewerThan(version)` and `OlderThan(version)` convenience methods, adds support for parsing a version string into a `SemanticVersion`, and fixes various bugs with the former implementation. This also replaces `Manifest` with `IManifest`._
+ _This new implementation is [semver 2.0](https://semver.org/)-compliant, introduces `NewerThan(version)` and `OlderThan(version)` convenience methods, adds support for parsing a version string into a `SemanticVersion`, and fixes various bugs with the former implementation. This also replaces `Manifest` with `IManifest`._
* Increased deprecation levels for `SObject`, `Extensions`, `LogWriter` (not `Log`), `SPlayer`, and `Mod.Entry(ModHelper)` (not `Mod.Entry(IModHelper)`).
## 1.4
@@ -771,7 +841,7 @@ For mod developers:
* Added OS version to log.
* Added zoom-adjusted mouse position to mouse-changed event arguments.
* Added SMAPI code documentation.
- * Switched to [semantic versioning](http://semver.org).
+ * Switched to [semantic versioning](https://semver.org).
* Fixed mod versions not shown correctly in the log.
* Fixed misspelled field in `manifest.json` schema.
* Fixed some events getting wrong data.
diff --git a/docs/technical-docs.md b/docs/technical-docs.md
index ed45871a..5883ee00 100644
--- a/docs/technical-docs.md
+++ b/docs/technical-docs.md
@@ -16,7 +16,7 @@ mods, this section isn't relevant to you; see the main README to use or create m
* [SMAPI web services](#smapi-web-services)
* [Overview](#overview)
* [Log parser](#log-parser)
- * [Mods API](#mods-api)
+ * [Web API](#web-api)
* [Development](#development-2)
* [Local development](#local-development)
* [Deploying to Amazon Beanstalk](#deploying-to-amazon-beanstalk)
@@ -29,7 +29,7 @@ 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,
+[MonoDevelop 7.0](https://www.monodevelop.com/) on Linux,
[Visual Studio 2017 for Mac](https://www.visualstudio.com/vs/visual-studio-mac/), or an equivalent
IDE to compile it. It uses build configuration derived from the
[crossplatform mod config](https://github.com/Pathoschild/Stardew.ModBuildConfig#readme) to detect
@@ -44,70 +44,29 @@ executed. This doesn't work in MonoDevelop on Linux, unfortunately.
### Preparing a release
To prepare a crossplatform SMAPI release, you'll need to compile it on two platforms. See
-[crossplatforming info](https://stardewvalleywiki.com/Modding:Creating_a_SMAPI_mod#Test_on_all_platforms)
+[crossplatforming info](https://stardewvalleywiki.com/Modding:Modder_Guide/Test_and_Troubleshoot#Testing_on_all_platforms)
on the wiki for the first-time setup.
1. Update the version number in `GlobalAssemblyInfo.cs` and `Constants::Version`. Make sure you use a
- [semantic version](http://semver.org). Recommended format:
+ [semantic version](https://semver.org). Recommended format:
- build type | format | example
- :--------- | :-------------------------------- | :------
- dev build | `-alpha.` | `2.0-alpha.20171230`
- prerelease | `-prerelease.` | `2.0-prerelease.2`
- release | `` | `2.0`
+ build type | format | example
+ :--------- | :----------------------- | :------
+ dev build | `-alpha.` | `3.0-alpha.20171230`
+ prerelease | `-beta.` | `3.0-beta.2`
+ release | `` | `3.0`
2. In Windows:
- 1. Rebuild the solution in _Release_ mode.
- 2. 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._
+ 1. Rebuild the solution in Release mode.
+ 2. Copy `windows-install.*` from `bin/SMAPI installer` and `bin/SMAPI installer for developers` to
+ Linux/Mac.
-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.config.json
- StardewModdingAPI.Internal.dll
- StardewModdingAPI.metadata.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.config.json
- StardewModdingAPI.Internal.dll
- StardewModdingAPI.metadata.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`.
+3. In Linux/Mac:
+ 1. Rebuild the solution in Release mode.
+ 2. Add the `windows-install.*` files to the `bin/SMAPI installer` and
+ `bin/SMAPI installer for developers` folders.
+ 3. Rename the folders to `SMAPI installer` and `SMAPI installer for developers`.
+ 4. Zip the two folders.
## Customisation
### Configuration file
diff --git a/src/SMAPI.Installer/Framework/InstallerPaths.cs b/src/SMAPI.Installer/Framework/InstallerPaths.cs
index d212876a..e5396018 100644
--- a/src/SMAPI.Installer/Framework/InstallerPaths.cs
+++ b/src/SMAPI.Installer/Framework/InstallerPaths.cs
@@ -8,8 +8,8 @@ namespace StardewModdingAPI.Installer.Framework
/*********
** Accessors
*********/
- /// The directory containing the installer files for the current platform.
- public DirectoryInfo PackageDir { get; }
+ /// The directory path containing the files to copy into the game folder.
+ public DirectoryInfo BundleDir { get; }
/// The directory containing the installed game.
public DirectoryInfo GameDir { get; }
@@ -17,8 +17,8 @@ namespace StardewModdingAPI.Installer.Framework
/// The directory into which to install mods.
public DirectoryInfo ModsDir { get; }
- /// The full path to the directory containing the installer files for the current platform.
- public string PackagePath => this.PackageDir.FullName;
+ /// The full path to directory path containing the files to copy into the game folder.
+ public string BundlePath => this.BundleDir.FullName;
/// The full path to the directory containing the installed game.
public string GamePath => this.GameDir.FullName;
@@ -26,6 +26,9 @@ namespace StardewModdingAPI.Installer.Framework
/// The full path to the directory into which to install mods.
public string ModsPath => this.ModsDir.FullName;
+ /// The full path to SMAPI's internal configuration file.
+ public string ApiConfigPath { get; }
+
/// The full path to the installed SMAPI executable file.
public string ExecutablePath { get; }
@@ -43,12 +46,12 @@ namespace StardewModdingAPI.Installer.Framework
** Public methods
*********/
/// Construct an instance.
- /// The directory path containing the installer files for the current platform.
+ /// The directory path containing the files to copy into the game folder.
/// The directory path for the installed game.
/// The name of the game's executable file for the current platform.
- public InstallerPaths(DirectoryInfo packageDir, DirectoryInfo gameDir, string gameExecutableName)
+ public InstallerPaths(DirectoryInfo bundleDir, DirectoryInfo gameDir, string gameExecutableName)
{
- this.PackageDir = packageDir;
+ this.BundleDir = bundleDir;
this.GameDir = gameDir;
this.ModsDir = new DirectoryInfo(Path.Combine(gameDir.FullName, "Mods"));
@@ -56,6 +59,7 @@ namespace StardewModdingAPI.Installer.Framework
this.UnixLauncherPath = Path.Combine(gameDir.FullName, "StardewValley");
this.UnixSmapiLauncherPath = Path.Combine(gameDir.FullName, "StardewModdingAPI");
this.UnixBackupLauncherPath = Path.Combine(gameDir.FullName, "StardewValley-original");
+ this.ApiConfigPath = Path.Combine(gameDir.FullName, "smapi-internal", "StardewModdingAPI.config.json");
}
}
}
diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs
index 0aac1da2..d5866c74 100644
--- a/src/SMAPI.Installer/InteractiveInstaller.cs
+++ b/src/SMAPI.Installer/InteractiveInstaller.cs
@@ -1,15 +1,18 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
-using System.Reflection;
using System.Threading;
using Microsoft.Win32;
using StardewModdingApi.Installer.Enums;
using StardewModdingAPI.Installer.Framework;
using StardewModdingAPI.Internal;
using StardewModdingAPI.Internal.ConsoleWriting;
+using StardewModdingAPI.Toolkit;
+using StardewModdingAPI.Toolkit.Framework.ModScanning;
+using StardewModdingAPI.Toolkit.Utilities;
namespace StardewModdingApi.Installer
{
@@ -19,18 +22,19 @@ namespace StardewModdingApi.Installer
/*********
** Properties
*********/
- /// The name of the installer file in the package.
- private readonly string InstallerFileName = "install.exe";
-
- /// Mod files which shouldn't be deleted when deploying bundled mods (mod folder name => file names).
- private readonly IDictionary> ProtectBundledFiles = new Dictionary>(StringComparer.InvariantCultureIgnoreCase)
- {
- ["SaveBackup"] = new HashSet(new[] { "backups", "config.json" }, StringComparer.InvariantCultureIgnoreCase)
- };
+ /// The absolute path to the directory containing the files to copy into the game folder.
+ private readonly string BundlePath;
/// The value that represents Windows 7.
private readonly Version Windows7Version = new Version(6, 1);
+ /// The mod IDs which the installer should allow as bundled mods.
+ private readonly string[] BundledModIDs = new[]
+ {
+ "SMAPI.SaveBackup",
+ "SMAPI.ConsoleCommands"
+ };
+
/// The default file paths where Stardew Valley can be installed.
/// The target platform.
/// Derived from the crossplatform mod config: https://github.com/Pathoschild/Stardew.ModBuildConfig.
@@ -58,8 +62,12 @@ namespace StardewModdingApi.Installer
case Platform.Windows:
{
// Windows
- yield return @"C:\Program Files (x86)\GalaxyClient\Games\Stardew Valley";
- yield return @"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley";
+ foreach (string programFiles in new[] { @"C:\Program Files", @"C:\Program Files (x86)" })
+ {
+ yield return $@"{programFiles}\GalaxyClient\Games\Stardew Valley";
+ yield return $@"{programFiles}\GOG Galaxy\Games\Stardew Valley";
+ yield return $@"{programFiles}\Steam\steamapps\common\Stardew Valley";
+ }
// Windows registry
IDictionary registryKeys = new Dictionary
@@ -93,40 +101,42 @@ namespace StardewModdingApi.Installer
{
string GetInstallPath(string path) => Path.Combine(installDir.FullName, path);
- // common
- yield return GetInstallPath("0Harmony.dll");
- yield return GetInstallPath("0Harmony.pdb");
- yield return GetInstallPath("Mono.Cecil.dll");
- yield return GetInstallPath("Newtonsoft.Json.dll");
+ // current files
+ yield return GetInstallPath("libgdiplus.dylib"); // Linux/Mac only
+ yield return GetInstallPath("StardewModdingAPI"); // Linux/Mac only
yield return GetInstallPath("StardewModdingAPI.exe");
- yield return GetInstallPath("StardewModdingAPI.config.json");
- yield return GetInstallPath("StardewModdingAPI.metadata.json");
- yield return GetInstallPath("StardewModdingAPI.Toolkit.dll");
- yield return GetInstallPath("StardewModdingAPI.Toolkit.pdb");
- yield return GetInstallPath("StardewModdingAPI.Toolkit.xml");
- yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.dll");
- yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.pdb");
- yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.xml");
+ yield return GetInstallPath("StardewModdingAPI.exe.config");
+ yield return GetInstallPath("StardewModdingAPI.exe.mdb"); // Linux/Mac only
+ yield return GetInstallPath("StardewModdingAPI.pdb"); // Windows only
yield return GetInstallPath("StardewModdingAPI.xml");
- yield return GetInstallPath("System.ValueTuple.dll");
- yield return GetInstallPath("steam_appid.txt");
-
- // Linux/Mac only
- yield return GetInstallPath("libgdiplus.dylib");
- yield return GetInstallPath("StardewModdingAPI");
- yield return GetInstallPath("StardewModdingAPI.exe.mdb");
- yield return GetInstallPath("System.Numerics.dll");
- yield return GetInstallPath("System.Runtime.Caching.dll");
-
- // Windows only
- yield return GetInstallPath("StardewModdingAPI.pdb");
+ yield return GetInstallPath("smapi-internal");
// obsolete
- yield return GetInstallPath(Path.Combine("Mods", ".cache")); // 1.3-1.4
+ yield return GetInstallPath(Path.Combine("Mods", ".cache")); // 1.3-1.4
yield return GetInstallPath(Path.Combine("Mods", "TrainerMod")); // *–2.0 (renamed to ConsoleCommands)
- yield return GetInstallPath("Mono.Cecil.Rocks.dll"); // 1.3–1.8
- yield return GetInstallPath("StardewModdingAPI-settings.json"); // 1.0-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-2.5.5
+ yield return GetInstallPath("0Harmony.dll"); // moved in 2.8
+ yield return GetInstallPath("0Harmony.pdb"); // moved in 2.8
+ yield return GetInstallPath("Mono.Cecil.dll"); // moved in 2.8
+ yield return GetInstallPath("Newtonsoft.Json.dll"); // moved in 2.8
+ yield return GetInstallPath("StardewModdingAPI.config.json"); // moved in 2.8
+ yield return GetInstallPath("StardewModdingAPI.crash.marker"); // moved in 2.8
+ yield return GetInstallPath("StardewModdingAPI.metadata.json"); // moved in 2.8
+ yield return GetInstallPath("StardewModdingAPI.update.marker"); // moved in 2.8
+ yield return GetInstallPath("StardewModdingAPI.Toolkit.dll"); // moved in 2.8
+ yield return GetInstallPath("StardewModdingAPI.Toolkit.pdb"); // moved in 2.8
+ yield return GetInstallPath("StardewModdingAPI.Toolkit.xml"); // moved in 2.8
+ yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.dll"); // moved in 2.8
+ yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.pdb"); // moved in 2.8
+ yield return GetInstallPath("StardewModdingAPI.Toolkit.CoreInterfaces.xml"); // moved in 2.8
+ yield return GetInstallPath("StardewModdingAPI.xml"); // moved in 2.8
+ yield return GetInstallPath("System.Numerics.dll"); // moved in 2.8
+ yield return GetInstallPath("System.Runtime.Caching.dll"); // moved in 2.8
+ yield return GetInstallPath("System.ValueTuple.dll"); // moved in 2.8
+ yield return GetInstallPath("steam_appid.txt"); // moved in 2.8
+
if (modsDir.Exists)
{
foreach (DirectoryInfo modDir in modsDir.EnumerateDirectories())
@@ -143,8 +153,10 @@ namespace StardewModdingApi.Installer
** Public methods
*********/
/// Construct an instance.
- public InteractiveInstaller()
+ /// The absolute path to the directory containing the files to copy into the game folder.
+ public InteractiveInstaller(string bundlePath)
{
+ this.BundlePath = bundlePath;
this.ConsoleWriter = new ColorfulConsoleWriter(EnvironmentUtility.DetectPlatform(), MonitorColorScheme.AutoDetect);
}
@@ -319,10 +331,8 @@ namespace StardewModdingApi.Installer
}
// get folders
- DirectoryInfo packageDir = platform.IsMono()
- ? new DirectoryInfo(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)) // installer runs from internal folder on Mono
- : new DirectoryInfo(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "internal", "Windows"));
- paths = new InstallerPaths(packageDir, installDir, EnvironmentUtility.GetExecutableName(platform));
+ DirectoryInfo bundleDir = new DirectoryInfo(this.BundlePath);
+ paths = new InstallerPaths(bundleDir, installDir, EnvironmentUtility.GetExecutableName(platform));
}
Console.Clear();
@@ -330,23 +340,11 @@ namespace StardewModdingApi.Installer
/*********
** Step 4: validate assumptions
*********/
+ if (!File.Exists(paths.ExecutablePath))
{
- if (!paths.PackageDir.Exists)
- {
- this.PrintError(platform == Platform.Windows && paths.PackagePath.Contains(Path.GetTempPath()) && paths.PackagePath.Contains(".zip")
- ? "The installer is missing some files. It looks like you're running the installer from inside the downloaded zip; make sure you unzip the downloaded file first, then run the installer from the unzipped folder."
- : $"The 'internal/{paths.PackageDir.Name}' package folder is missing (should be at {paths.PackagePath})."
- );
- Console.ReadLine();
- return;
- }
-
- if (!File.Exists(paths.ExecutablePath))
- {
- this.PrintError("The detected game install path doesn't contain a Stardew Valley executable.");
- Console.ReadLine();
- return;
- }
+ this.PrintError("The detected game install path doesn't contain a Stardew Valley executable.");
+ Console.ReadLine();
+ return;
}
@@ -438,20 +436,18 @@ namespace StardewModdingApi.Installer
{
// copy SMAPI files to game dir
this.PrintDebug("Adding SMAPI files...");
- foreach (FileInfo sourceFile in paths.PackageDir.EnumerateFiles().Where(this.ShouldCopyFile))
+ foreach (FileSystemInfo sourceEntry in paths.BundleDir.EnumerateFileSystemInfos().Where(this.ShouldCopy))
{
- if (sourceFile.Name == this.InstallerFileName)
- continue;
-
- string targetPath = Path.Combine(paths.GameDir.FullName, sourceFile.Name);
- this.InteractivelyDelete(targetPath);
- sourceFile.CopyTo(targetPath);
+ this.InteractivelyDelete(Path.Combine(paths.GameDir.FullName, sourceEntry.Name));
+ this.RecursiveCopy(sourceEntry, paths.GameDir);
}
// replace mod launcher (if possible)
if (platform.IsMono())
{
this.PrintDebug("Safely replacing game launcher...");
+
+ // back up & remove current launcher
if (File.Exists(paths.UnixLauncherPath))
{
if (!File.Exists(paths.UnixBackupLauncherPath))
@@ -460,7 +456,20 @@ namespace StardewModdingApi.Installer
this.InteractivelyDelete(paths.UnixLauncherPath);
}
+ // add new launcher
File.Move(paths.UnixSmapiLauncherPath, paths.UnixLauncherPath);
+
+ // mark file executable
+ // (MSBuild doesn't keep permission flags for files zipped in a build task.)
+ new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ FileName = "chmod",
+ Arguments = $"755 \"{paths.UnixLauncherPath}\"",
+ CreateNoWindow = true
+ }
+ }.Start();
}
// create mods directory (if needed)
@@ -471,60 +480,56 @@ namespace StardewModdingApi.Installer
}
// add or replace bundled mods
- DirectoryInfo packagedModsDir = new DirectoryInfo(Path.Combine(paths.PackageDir.FullName, "Mods"));
- if (packagedModsDir.Exists && packagedModsDir.EnumerateDirectories().Any())
+ DirectoryInfo bundledModsDir = new DirectoryInfo(Path.Combine(paths.BundlePath, "Mods"));
+ if (bundledModsDir.Exists && bundledModsDir.EnumerateDirectories().Any())
{
this.PrintDebug("Adding bundled mods...");
- // special case: rename Omegasis' SaveBackup mod
+ ModToolkit toolkit = new ModToolkit();
+ ModFolder[] targetMods = toolkit.GetModFolders(paths.ModsPath).ToArray();
+ foreach (ModFolder sourceMod in toolkit.GetModFolders(bundledModsDir.FullName))
{
- DirectoryInfo oldFolder = new DirectoryInfo(Path.Combine(paths.ModsDir.FullName, "SaveBackup"));
- DirectoryInfo newFolder = new DirectoryInfo(Path.Combine(paths.ModsDir.FullName, "AdvancedSaveBackup"));
- FileInfo manifest = new FileInfo(Path.Combine(oldFolder.FullName, "manifest.json"));
- if (manifest.Exists && !newFolder.Exists && File.ReadLines(manifest.FullName).Any(p => p.IndexOf("Omegasis", StringComparison.InvariantCultureIgnoreCase) != -1))
+ // validate source mod
+ if (sourceMod.Manifest == null)
{
- this.PrintDebug($" moving {oldFolder.Name} to {newFolder.Name}...");
- this.Move(oldFolder, newFolder.FullName);
+ this.PrintWarning($" ignored invalid bundled mod {sourceMod.DisplayName}: {sourceMod.ManifestParseError}");
+ continue;
}
- }
-
- // add bundled mods
- foreach (DirectoryInfo sourceDir in packagedModsDir.EnumerateDirectories())
- {
- this.PrintDebug($" adding {sourceDir.Name}...");
-
- // init/clear target dir
- DirectoryInfo targetDir = new DirectoryInfo(Path.Combine(paths.ModsDir.FullName, sourceDir.Name));
- if (targetDir.Exists)
+ if (!this.BundledModIDs.Contains(sourceMod.Manifest.UniqueID))
{
- this.ProtectBundledFiles.TryGetValue(targetDir.Name, out HashSet protectedFiles);
- foreach (FileSystemInfo entry in targetDir.EnumerateFileSystemInfos())
- {
- if (protectedFiles == null || !protectedFiles.Contains(entry.Name))
- this.InteractivelyDelete(entry.FullName);
- }
+ this.PrintWarning($" ignored unknown '{sourceMod.DisplayName}' mod in the installer folder. To add mods, put them here instead: {paths.ModsPath}");
+ continue;
}
- else
- targetDir.Create();
+
+ // find target folder
+ ModFolder targetMod = targetMods.FirstOrDefault(p => p.Manifest?.UniqueID?.Equals(sourceMod.Manifest.UniqueID, StringComparison.InvariantCultureIgnoreCase) == true);
+ DirectoryInfo defaultTargetFolder = new DirectoryInfo(Path.Combine(paths.ModsPath, sourceMod.Directory.Name));
+ DirectoryInfo targetFolder = targetMod?.Directory ?? defaultTargetFolder;
+ this.PrintDebug(targetFolder.FullName == defaultTargetFolder.FullName
+ ? $" adding {sourceMod.Manifest.Name}..."
+ : $" adding {sourceMod.Manifest.Name} to {Path.Combine(paths.ModsDir.Name, PathUtilities.GetRelativePath(paths.ModsPath, targetFolder.FullName))}..."
+ );
+
+ // remove existing folder
+ if (targetFolder.Exists)
+ this.InteractivelyDelete(targetFolder.FullName);
// copy files
- foreach (FileInfo sourceFile in sourceDir.EnumerateFiles().Where(this.ShouldCopyFile))
- sourceFile.CopyTo(Path.Combine(targetDir.FullName, sourceFile.Name));
- }
-
- // set SMAPI's color scheme if defined
- if (scheme != MonitorColorScheme.AutoDetect)
- {
- string configPath = Path.Combine(paths.GamePath, "StardewModdingAPI.config.json");
- string text = File
- .ReadAllText(configPath)
- .Replace(@"""ColorScheme"": ""AutoDetect""", $@"""ColorScheme"": ""{scheme}""");
- File.WriteAllText(configPath, text);
+ this.RecursiveCopy(sourceMod.Directory, paths.ModsDir, filter: this.ShouldCopy);
}
}
+ // set SMAPI's color scheme if defined
+ if (scheme != MonitorColorScheme.AutoDetect)
+ {
+ string text = File
+ .ReadAllText(paths.ApiConfigPath)
+ .Replace(@"""ColorScheme"": ""AutoDetect""", $@"""ColorScheme"": ""{scheme}""");
+ File.WriteAllText(paths.ApiConfigPath, text);
+ }
+
// remove obsolete appdata mods
- this.InteractivelyRemoveAppDataMods(paths.ModsDir, packagedModsDir);
+ this.InteractivelyRemoveAppDataMods(paths.ModsDir, bundledModsDir);
}
}
Console.WriteLine();
@@ -690,6 +695,35 @@ namespace StardewModdingApi.Installer
}
}
+ /// Recursively copy a directory or file.
+ /// The file or folder to copy.
+ /// The folder to copy into.
+ /// A filter which matches directories and files to copy, or null to match all.
+ private void RecursiveCopy(FileSystemInfo source, DirectoryInfo targetFolder, Func filter = null)
+ {
+ if (filter != null && !filter(source))
+ return;
+
+ if (!targetFolder.Exists)
+ targetFolder.Create();
+
+ switch (source)
+ {
+ case FileInfo sourceFile:
+ sourceFile.CopyTo(Path.Combine(targetFolder.FullName, sourceFile.Name));
+ break;
+
+ case DirectoryInfo sourceDir:
+ DirectoryInfo targetSubfolder = new DirectoryInfo(Path.Combine(targetFolder.FullName, sourceDir.Name));
+ foreach (var entry in sourceDir.EnumerateFileSystemInfos())
+ this.RecursiveCopy(entry, targetSubfolder, filter);
+ break;
+
+ default:
+ throw new NotSupportedException($"Unknown filesystem info type '{source.GetType().FullName}'.");
+ }
+ }
+
/// Delete a file or folder regardless of file permissions, and block until deletion completes.
/// The file or folder to reset.
/// This method is mirred from FileUtilities.ForceDelete in the toolkit.
@@ -871,7 +905,7 @@ namespace StardewModdingApi.Installer
this.PrintDebug(" Support for mods here was dropped in SMAPI 1.0 (it was never officially supported).");
// move mods if no conflicts (else warn)
- foreach (FileSystemInfo entry in modDir.EnumerateFileSystemInfos().Where(this.ShouldCopyFile))
+ foreach (FileSystemInfo entry in modDir.EnumerateFileSystemInfos().Where(this.ShouldCopy))
{
// get type
bool isDir = entry is DirectoryInfo;
@@ -928,22 +962,26 @@ namespace StardewModdingApi.Installer
Directory.CreateDirectory(newPath);
DirectoryInfo directory = (DirectoryInfo)entry;
- foreach (FileSystemInfo child in directory.EnumerateFileSystemInfos().Where(this.ShouldCopyFile))
+ foreach (FileSystemInfo child in directory.EnumerateFileSystemInfos().Where(this.ShouldCopy))
this.Move(child, Path.Combine(newPath, child.Name));
directory.Delete(recursive: true);
}
}
- /// Get whether a file should be copied when moving a folder.
- /// The file info.
- private bool ShouldCopyFile(FileSystemInfo file)
+ /// Get whether a file or folder should be copied from the installer files.
+ /// The file or folder info.
+ private bool ShouldCopy(FileSystemInfo entry)
{
- // ignore Mac symlink
- if (file is FileInfo && file.Name == "mcs")
- return false;
-
- return true;
+ switch (entry.Name)
+ {
+ case "mcs":
+ return false; // ignore Mac symlink
+ case "Mods":
+ return false; // Mods folder handled separately
+ default:
+ return true;
+ }
}
}
}
diff --git a/src/SMAPI.Installer/Program.cs b/src/SMAPI.Installer/Program.cs
index 8f328ecf..0ca5aea0 100644
--- a/src/SMAPI.Installer/Program.cs
+++ b/src/SMAPI.Installer/Program.cs
@@ -1,8 +1,29 @@
-namespace StardewModdingApi.Installer
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.IO.Compression;
+using System.Reflection;
+using StardewModdingAPI.Internal;
+using StardewModdingAPI.Toolkit.Utilities;
+
+namespace StardewModdingApi.Installer
{
/// The entry point for SMAPI's install and uninstall console app.
internal class Program
{
+ /*********
+ ** Properties
+ *********/
+ /// The absolute path of the installer folder.
+ [SuppressMessage("ReSharper", "AssignNullToNotNullAttribute", Justification = "The assembly location is never null in this context.")]
+ private static readonly string InstallerPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
+
+ /// The absolute path of the folder containing the unzipped installer files.
+ private static readonly string ExtractedBundlePath = Path.Combine(Path.GetTempPath(), $"SMAPI-installer-{Guid.NewGuid():N}");
+
+ /// The absolute path for referenced assemblies.
+ private static readonly string InternalFilesPath = Path.Combine(Program.ExtractedBundlePath, "smapi-internal");
+
/*********
** Public methods
*********/
@@ -10,8 +31,52 @@
/// The command line arguments.
public static void Main(string[] args)
{
- var installer = new InteractiveInstaller();
+ // find install bundle
+ PlatformID platform = Environment.OSVersion.Platform;
+ FileInfo zipFile = new FileInfo(Path.Combine(Program.InstallerPath, $"{(platform == PlatformID.Win32NT ? "windows" : "unix")}-install.dat"));
+ if (!zipFile.Exists)
+ {
+ Console.WriteLine($"Oops! Some of the installer files are missing; try redownloading the installer. (Missing file: {zipFile.FullName})");
+ Console.ReadLine();
+ return;
+ }
+
+ // unzip bundle into temp folder
+ DirectoryInfo bundleDir = new DirectoryInfo(Program.ExtractedBundlePath);
+ Console.WriteLine("Extracting install files...");
+ ZipFile.ExtractToDirectory(zipFile.FullName, bundleDir.FullName);
+
+ // set up assembly resolution
+ AppDomain.CurrentDomain.AssemblyResolve += Program.CurrentDomain_AssemblyResolve;
+
+ // launch installer
+ var installer = new InteractiveInstaller(bundleDir.FullName);
installer.Run(args);
}
+
+ /*********
+ ** Private methods
+ *********/
+ /// Method called when assembly resolution fails, which may return a manually resolved assembly.
+ /// The event sender.
+ /// The event arguments.
+ private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs e)
+ {
+ try
+ {
+ AssemblyName name = new AssemblyName(e.Name);
+ foreach (FileInfo dll in new DirectoryInfo(Program.InternalFilesPath).EnumerateFiles("*.dll"))
+ {
+ if (name.Name.Equals(AssemblyName.GetAssemblyName(dll.FullName).Name, StringComparison.InvariantCultureIgnoreCase))
+ return Assembly.LoadFrom(dll.FullName);
+ }
+ return null;
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error resolving assembly: {ex}");
+ return null;
+ }
+ }
}
}
diff --git a/src/SMAPI.Installer/readme.txt b/src/SMAPI.Installer/README.txt
similarity index 62%
rename from src/SMAPI.Installer/readme.txt
rename to src/SMAPI.Installer/README.txt
index 2ee5473c..79c90cc0 100644
--- a/src/SMAPI.Installer/readme.txt
+++ b/src/SMAPI.Installer/README.txt
@@ -16,7 +16,7 @@ SMAPI lets you run Stardew Valley with mods. Don't forget to download mods separ
Player's guide
--------------------------------
-See https://stardewvalleywiki.com/Modding:Player_Guide
+See https://stardewvalleywiki.com/Modding:Player_Guide for help installing SMAPI, adding mods, etc.
Manual install
@@ -24,15 +24,21 @@ Manual install
THIS IS NOT RECOMMENDED FOR MOST PLAYERS. See instructions above instead.
If you really want to install SMAPI manually, here's how.
-1. Download the latest version of SMAPI: https://github.com/Pathoschild/SMAPI/releases
-2. Unzip the .zip file somewhere (not in your game folder).
-3. Copy the files from the "internal/Windows" folder (on Windows) or "internal/Mono" folder (on
- Linux/Mac) into your game folder. The `StardewModdingAPI.exe` file should be right next to the
- game's executable.
-4.
+1. Unzip "internal/windows-install.dat" (on Windows) or "internal/unix-install.dat" (on Linux/Mac).
+ You can change '.dat' to '.zip', it's just a normal zip file renamed to prevent confusion.
+2. Copy the files from the folder you just unzipped into your game folder. The
+ `StardewModdingAPI.exe` file should be right next to the game's executable.
+3.
- Windows only: if you use Steam, see the install guide above to enable achievements and
overlay. Otherwise, just run StardewModdingAPI.exe in your game folder to play with mods.
- Linux/Mac only: rename the "StardewValley" file (no extension) to "StardewValley-original", and
"StardewModdingAPI" (no extension) to "StardewValley". Now just launch the game as usual to
play with mods.
+
+When installing on Linux or Mac:
+- Make sure Mono is installed (normally the installer checks for you). While it's not required,
+ many mods won't work correctly without it. (Specifically, mods which load PNG images may crash or
+ freeze the game.)
+- To configure the color scheme, edit the `smapi-internal/StardewModdingAPI.config.json` file and
+ see instructions there for the 'ColorScheme' setting.
diff --git a/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj b/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj
index e82c6093..8000e4e7 100644
--- a/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj
+++ b/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj
@@ -34,6 +34,8 @@
+
+
@@ -46,11 +48,17 @@
-
+ Always
+
+ PreserveNewest
+
+
+ PreserveNewest
+ PreserveNewest
@@ -58,6 +66,12 @@
PreserveNewest
+
+
+ {ea5cfd2e-9453-4d29-b80f-8e0ea23f4ac6}
+ StardewModdingAPI.Toolkit
+
+
diff --git a/src/SMAPI.Installer/unix-install.sh b/src/SMAPI.Installer/unix-install.sh
index df02bb37..e3a5d8cc 100644
--- a/src/SMAPI.Installer/unix-install.sh
+++ b/src/SMAPI.Installer/unix-install.sh
@@ -14,8 +14,8 @@ fi
# validate Mono & run installer
if $COMMAND mono >/dev/null 2>&1; then
- mono internal/Mono/install.exe
+ mono internal/unix-install.exe
else
- echo "Oops! Looks like Mono isn't installed. Please install Mono from http://mono-project.com, reboot, and run this installer again."
+ echo "Oops! Looks like Mono isn't installed. Please install Mono from https://mono-project.com, reboot, and run this installer again."
read
fi
diff --git a/src/SMAPI.Installer/unix-launcher.sh b/src/SMAPI.Installer/unix-launcher.sh
index 1e969c20..3c332472 100644
--- a/src/SMAPI.Installer/unix-launcher.sh
+++ b/src/SMAPI.Installer/unix-launcher.sh
@@ -62,27 +62,27 @@ else
fi
# open SMAPI in terminal
- if $COMMAND x-terminal-emulator 2>/dev/null; then
+ if $COMMAND xterm 2>/dev/null; then
+ xterm -e "$LAUNCHER"
+ elif $COMMAND x-terminal-emulator 2>/dev/null; then
# Terminator converts -e to -x when used through x-terminal-emulator for some reason (per
# `man terminator`), which causes an "unable to find shell" error. If x-terminal-emulator
# is mapped to Terminator, invoke it directly instead.
if [[ "$(readlink -e $(which x-terminal-emulator))" == *"/terminator" ]]; then
- terminator -e "$LAUNCHER"
+ terminator -e "sh -c 'TERM=xterm $LAUNCHER'"
else
- x-terminal-emulator -e "$LAUNCHER"
+ x-terminal-emulator -e "sh -c 'TERM=xterm $LAUNCHER'"
fi
- elif $COMMAND xterm 2>/dev/null; then
- xterm -e "$LAUNCHER"
elif $COMMAND xfce4-terminal 2>/dev/null; then
- xfce4-terminal -e "env TERM=xterm; $LAUNCHER"
+ xfce4-terminal -e "sh -c 'TERM=xterm $LAUNCHER'"
elif $COMMAND gnome-terminal 2>/dev/null; then
- gnome-terminal -e "env TERM=xterm; $LAUNCHER"
+ gnome-terminal -e "sh -c 'TERM=xterm $LAUNCHER'"
elif $COMMAND konsole 2>/dev/null; then
konsole -p Environment=TERM=xterm -e "$LAUNCHER"
elif $COMMAND terminal 2>/dev/null; then
- terminal -e "$LAUNCHER"
+ terminal -e "sh -c 'TERM=xterm $LAUNCHER'"
else
- $LAUNCHER
+ sh -c 'TERM=xterm $LAUNCHER'
fi
# some Linux users get error 127 (command not found) from the above block, even though
diff --git a/src/SMAPI.Installer/windows-exe-config.xml b/src/SMAPI.Installer/windows-exe-config.xml
new file mode 100644
index 00000000..386c7f1a
--- /dev/null
+++ b/src/SMAPI.Installer/windows-exe-config.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/SMAPI.Installer/windows-install.bat b/src/SMAPI.Installer/windows-install.bat
new file mode 100644
index 00000000..7a8b409b
--- /dev/null
+++ b/src/SMAPI.Installer/windows-install.bat
@@ -0,0 +1 @@
+START /WAIT /B internal/windows-install.exe
diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj b/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj
index c6241ecb..4d93df73 100644
--- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj
+++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj
@@ -6,9 +6,9 @@
-
-
-
+
+
+
diff --git a/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs b/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs
index f4738d71..7ff66695 100644
--- a/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs
+++ b/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs
@@ -3,8 +3,8 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
-using System.Web.Script.Serialization;
-using StardewModdingAPI.Toolkit;
+using StardewModdingAPI.Toolkit.Serialisation;
+using StardewModdingAPI.Toolkit.Serialisation.Models;
namespace StardewModdingAPI.ModBuildConfig.Framework
{
@@ -107,41 +107,10 @@ namespace StardewModdingAPI.ModBuildConfig.Framework
/// The manifest is missing or invalid.
public string GetManifestVersion()
{
- // get manifest file
- if (!this.Files.TryGetValue(this.ManifestFileName, out FileInfo manifestFile))
+ if (!this.Files.TryGetValue(this.ManifestFileName, out FileInfo manifestFile) || !new JsonHelper().ReadJsonFileIfExists(manifestFile.FullName, out Manifest manifest))
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 SemanticVersion(major, minor, patch, tag).ToString();
- }
- return new SemanticVersion(versionObj.ToString()).ToString(); // SMAPI 2.0+
+ return manifest.Version.ToString();
}
@@ -174,24 +143,6 @@ namespace StardewModdingAPI.ModBuildConfig.Framework
|| ignoreFilePatterns.Any(p => p.IsMatch(relativePath));
}
- /// 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 whether a string is equal to another case-insensitively.
/// The string value.
/// The string to compare with.
diff --git a/src/SMAPI.ModBuildConfig/Properties/AssemblyInfo.cs b/src/SMAPI.ModBuildConfig/Properties/AssemblyInfo.cs
index d6f8dd7f..e051bfbd 100644
--- a/src/SMAPI.ModBuildConfig/Properties/AssemblyInfo.cs
+++ b/src/SMAPI.ModBuildConfig/Properties/AssemblyInfo.cs
@@ -2,5 +2,5 @@ using System.Reflection;
[assembly: AssemblyTitle("SMAPI.ModBuildConfig")]
[assembly: AssemblyDescription("")]
-[assembly: AssemblyVersion("2.1.0.0")]
-[assembly: AssemblyFileVersion("2.1.0.0")]
+[assembly: AssemblyVersion("2.2.0")]
+[assembly: AssemblyFileVersion("2.2.0")]
diff --git a/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj b/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj
index 6a52daac..f068b480 100644
--- a/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj
+++ b/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj
@@ -56,6 +56,10 @@
+
+ {d5cfd923-37f1-4bc3-9be8-e506e202ac28}
+ StardewModdingAPI.Toolkit.CoreInterfaces
+ {ea5cfd2e-9453-4d29-b80f-8e0ea23f4ac6}StardewModdingAPI.Toolkit
diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets
index d1c8a4eb..e6c3fa57 100644
--- a/src/SMAPI.ModBuildConfig/build/smapi.targets
+++ b/src/SMAPI.ModBuildConfig/build/smapi.targets
@@ -42,6 +42,10 @@
+ C:\Program Files\GalaxyClient\Games\Stardew Valley
+ C:\Program Files\GOG Galaxy\Games\Stardew Valley
+ C:\Program Files\Steam\steamapps\common\Stardew Valley
+
C:\Program Files (x86)\GalaxyClient\Games\Stardew ValleyC:\Program Files (x86)\GOG Galaxy\Games\Stardew ValleyC:\Program Files (x86)\Steam\steamapps\common\Stardew Valley
@@ -97,7 +101,8 @@
true
- $(GamePath)\StardewModdingAPI.Toolkit.CoreInterfaces.dll
+ $(GamePath)\smapi-internal\StardewModdingAPI.Toolkit.CoreInterfaces.dll
+ $(GamePath)\StardewModdingAPI.Toolkit.CoreInterfaces.dllfalsetrue
@@ -136,7 +141,8 @@
true
- $(GamePath)\StardewModdingAPI.Toolkit.CoreInterfaces.dll
+ $(GamePath)\smapi-internal\StardewModdingAPI.Toolkit.CoreInterfaces.dll
+ $(GamePath)\StardewModdingAPI.Toolkit.CoreInterfaces.dllfalsetrue
diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec
index 3d6f2598..21693828 100644
--- a/src/SMAPI.ModBuildConfig/package.nuspec
+++ b/src/SMAPI.ModBuildConfig/package.nuspec
@@ -2,7 +2,7 @@
Pathoschild.Stardew.ModBuildConfig
- 2.1.0
+ 2.2Build package for SMAPI modsPathoschildPathoschild
@@ -12,13 +12,10 @@
https://raw.githubusercontent.com/Pathoschild/SMAPI/develop/src/SMAPI.ModBuildConfig/assets/nuget-icon.pngAutomates the build configuration for crossplatform Stardew Valley SMAPI mods. For Stardew Valley 1.3 or later.
- 2.1:
- - Added support for Stardew Valley 1.3.
- - Added support for non-mod projects.
- - Added C# analyzers to warn about implicit conversions of Netcode fields in Stardew Valley 1.3.
- - Added option to ignore files by regex pattern.
- - Added reference to new SMAPI DLL.
- - Fixed some game paths not detected by NuGet package.
+ 2.2:
+ - Added support for SMAPI 2.8+ (still compatible with earlier versions).
+ - Added default game paths for 32-bit Windows.
+ - Fixed valid manifests marked invalid in some cases.
diff --git a/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj b/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj
index 50b7b87f..1137bb11 100644
--- a/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj
+++ b/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj
@@ -36,10 +36,6 @@
x86
-
- ..\packages\Newtonsoft.Json.11.0.2\lib\net45\Newtonsoft.Json.dll
- False
-
@@ -89,7 +85,6 @@
PreserveNewest
-
diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json
index a6c5cd88..3b00aa83 100644
--- a/src/SMAPI.Mods.ConsoleCommands/manifest.json
+++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json
@@ -1,9 +1,9 @@
{
"Name": "Console Commands",
"Author": "SMAPI",
- "Version": "2.7.0",
+ "Version": "2.8.0",
"Description": "Adds SMAPI console commands that let you manipulate the game.",
"UniqueID": "SMAPI.ConsoleCommands",
"EntryDll": "ConsoleCommands.dll",
- "MinimumApiVersion": "2.7.0"
+ "MinimumApiVersion": "2.8.1"
}
diff --git a/src/SMAPI.Mods.ConsoleCommands/packages.config b/src/SMAPI.Mods.ConsoleCommands/packages.config
deleted file mode 100644
index c8b3ae63..00000000
--- a/src/SMAPI.Mods.ConsoleCommands/packages.config
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/src/SMAPI.Mods.SaveBackup/Framework/ModConfig.cs b/src/SMAPI.Mods.SaveBackup/Framework/ModConfig.cs
deleted file mode 100644
index c9dcb216..00000000
--- a/src/SMAPI.Mods.SaveBackup/Framework/ModConfig.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace StardewModdingAPI.Mods.SaveBackup.Framework
-{
- /// The mod configuration.
- internal class ModConfig
- {
- /// The number of backups to keep.
- public int BackupsToKeep { get; set; } = 10;
- }
-}
diff --git a/src/SMAPI.Mods.SaveBackup/ModEntry.cs b/src/SMAPI.Mods.SaveBackup/ModEntry.cs
index 78578c3c..4d56789a 100644
--- a/src/SMAPI.Mods.SaveBackup/ModEntry.cs
+++ b/src/SMAPI.Mods.SaveBackup/ModEntry.cs
@@ -4,7 +4,6 @@ using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Reflection;
-using StardewModdingAPI.Mods.SaveBackup.Framework;
using StardewValley;
namespace StardewModdingAPI.Mods.SaveBackup
@@ -15,6 +14,12 @@ namespace StardewModdingAPI.Mods.SaveBackup
/*********
** Properties
*********/
+ /// The number of backups to keep.
+ private readonly int BackupsToKeep = 10;
+
+ /// The absolute path to the folder in which to store save backups.
+ private readonly string BackupFolder = Path.Combine(Constants.ExecutionPath, "save-backups");
+
/// The name of the save archive to create.
private readonly string FileName = $"{DateTime.UtcNow:yyyy-MM-dd} - SMAPI {Constants.ApiVersion} with Stardew Valley {Game1.version}.zip";
@@ -28,15 +33,13 @@ namespace StardewModdingAPI.Mods.SaveBackup
{
try
{
- ModConfig config = this.Helper.ReadConfig();
-
// init backup folder
- DirectoryInfo backupFolder = new DirectoryInfo(Path.Combine(this.Helper.DirectoryPath, "backups"));
+ DirectoryInfo backupFolder = new DirectoryInfo(this.BackupFolder);
backupFolder.Create();
// back up saves
this.CreateBackup(backupFolder);
- this.PruneBackups(backupFolder, config.BackupsToKeep);
+ this.PruneBackups(backupFolder, this.BackupsToKeep);
}
catch (Exception ex)
{
diff --git a/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj b/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj
index 0ccbcc6c..fafa4d25 100644
--- a/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj
+++ b/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj
@@ -1,4 +1,4 @@
-
+
@@ -36,7 +36,6 @@
Properties\GlobalAssemblyInfo.cs
-
diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json
index e973b449..f0c67056 100644
--- a/src/SMAPI.Mods.SaveBackup/manifest.json
+++ b/src/SMAPI.Mods.SaveBackup/manifest.json
@@ -1,9 +1,9 @@
{
"Name": "Save Backup",
"Author": "SMAPI",
- "Version": "2.7.0",
+ "Version": "2.8.0",
"Description": "Automatically backs up all your saves once per day into its folder.",
"UniqueID": "SMAPI.SaveBackup",
"EntryDll": "SaveBackup.dll",
- "MinimumApiVersion": "2.7.0"
+ "MinimumApiVersion": "2.8.1"
}
diff --git a/src/SMAPI.Tests/Core/ModResolverTests.cs b/src/SMAPI.Tests/Core/ModResolverTests.cs
index a38621f8..4a1f04c6 100644
--- a/src/SMAPI.Tests/Core/ModResolverTests.cs
+++ b/src/SMAPI.Tests/Core/ModResolverTests.cs
@@ -145,7 +145,7 @@ namespace StardewModdingAPI.Tests.Core
this.SetupMetadataForValidation(mock, new ModDataRecordVersionedFields
{
Status = ModStatus.AssumeBroken,
- AlternativeUrl = "http://example.org"
+ AlternativeUrl = "https://example.org"
});
// act
@@ -513,6 +513,7 @@ namespace StardewModdingAPI.Tests.Core
mod.Setup(p => p.Status).Returns(ModMetadataStatus.Found);
mod.Setup(p => p.DisplayName).Returns(manifest.UniqueID);
mod.Setup(p => p.Manifest).Returns(manifest);
+ mod.Setup(p => p.HasID(It.IsAny())).Returns((string id) => manifest.UniqueID == id);
if (allowStatusChange)
{
mod
diff --git a/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj b/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj
index b2d98d23..4ec1a3de 100644
--- a/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj
+++ b/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj
@@ -1,6 +1,5 @@
- Debug
@@ -31,29 +30,17 @@
4
-
- ..\packages\Castle.Core.4.3.1\lib\net45\Castle.Core.dll
-
-
- ..\packages\Moq.4.8.3\lib\net45\Moq.dll
-
-
- ..\packages\Newtonsoft.Json.11.0.2\lib\net45\Newtonsoft.Json.dll
-
-
- ..\packages\NUnit.3.10.1\lib\net45\nunit.framework.dll
-
+
+
+
+
+
+
+
+
+
-
- ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.1\lib\netstandard1.0\System.Runtime.CompilerServices.Unsafe.dll
-
-
- ..\packages\System.Threading.Tasks.Extensions.4.5.1\lib\portable-net45+win8+wp8+wpa81\System.Threading.Tasks.Extensions.dll
-
-
- ..\packages\System.ValueTuple.4.5.0\lib\netstandard1.0\System.ValueTuple.dll
-
@@ -69,7 +56,6 @@
-
@@ -90,10 +76,4 @@
-
-
- This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.
-
-
-
\ No newline at end of file
diff --git a/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs b/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs
index 35d74b60..1782308b 100644
--- a/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs
+++ b/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs
@@ -127,6 +127,7 @@ namespace StardewModdingAPI.Tests.Utilities
[TestCase("1.0-beta.1", "1.0-beta.2", ExpectedResult = -1)]
[TestCase("1.0-beta.2", "1.0-beta.10", ExpectedResult = -1)]
[TestCase("1.0-beta-2", "1.0-beta-10", ExpectedResult = -1)]
+ [TestCase("1.0-unofficial.1", "1.0-beta.1", ExpectedResult = -1)] // special case: 'unofficial' has lower priority than official releases
// more than
[TestCase("0.5.8", "0.5.7", ExpectedResult = 1)]
diff --git a/src/SMAPI.Tests/packages.config b/src/SMAPI.Tests/packages.config
deleted file mode 100644
index 7c3ec9f1..00000000
--- a/src/SMAPI.Tests/packages.config
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/SMAPI.Web/Controllers/IndexController.cs b/src/SMAPI.Web/Controllers/IndexController.cs
index 8c4a0332..d7be664d 100644
--- a/src/SMAPI.Web/Controllers/IndexController.cs
+++ b/src/SMAPI.Web/Controllers/IndexController.cs
@@ -67,15 +67,22 @@ namespace StardewModdingAPI.Web.Controllers
IndexVersionModel stableVersionModel = stableVersion != null
? new IndexVersionModel(stableVersion.Version.ToString(), stableVersion.Release.Body, stableVersion.Asset.DownloadUrl, stableVersionForDevs?.Asset.DownloadUrl)
: new IndexVersionModel("unknown", "", "https://github.com/Pathoschild/SMAPI/releases", null); // just in case something goes wrong)
- IndexVersionModel betaVersionModel = betaVersion != null && this.SiteConfig.EnableSmapiBeta
+ IndexVersionModel betaVersionModel = betaVersion != null && this.SiteConfig.BetaEnabled
? new IndexVersionModel(betaVersion.Version.ToString(), betaVersion.Release.Body, betaVersion.Asset.DownloadUrl, betaVersionForDevs?.Asset.DownloadUrl)
: null;
// render view
- var model = new IndexModel(stableVersionModel, betaVersionModel);
+ var model = new IndexModel(stableVersionModel, betaVersionModel, this.SiteConfig.BetaBlurb);
return this.View(model);
}
+ /// Display the index page.
+ [HttpGet("/privacy")]
+ public ViewResult Privacy()
+ {
+ return this.View();
+ }
+
/*********
** Private methods
diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs
index 18d55665..f0835592 100644
--- a/src/SMAPI.Web/Controllers/ModsApiController.cs
+++ b/src/SMAPI.Web/Controllers/ModsApiController.cs
@@ -12,6 +12,7 @@ using StardewModdingAPI.Toolkit;
using StardewModdingAPI.Toolkit.Framework.Clients.WebApi;
using StardewModdingAPI.Toolkit.Framework.Clients.Wiki;
using StardewModdingAPI.Toolkit.Framework.ModData;
+using StardewModdingAPI.Toolkit.Framework.UpdateData;
using StardewModdingAPI.Web.Framework.Clients.Chucklefish;
using StardewModdingAPI.Web.Framework.Clients.GitHub;
using StardewModdingAPI.Web.Framework.Clients.Nexus;
@@ -29,7 +30,7 @@ namespace StardewModdingAPI.Web.Controllers
** Properties
*********/
/// The mod repositories which provide mod metadata.
- private readonly IDictionary Repositories;
+ private readonly IDictionary Repositories;
/// The cache in which to store mod metadata.
private readonly IMemoryCache Cache;
@@ -46,8 +47,8 @@ namespace StardewModdingAPI.Web.Controllers
/// The internal mod metadata list.
private readonly ModDatabase ModDatabase;
- /// The web URL for the wiki compatibility list.
- private readonly string WikiCompatibilityPageUrl;
+ /// The web URL for the compatibility list.
+ private readonly string CompatibilityPageUrl;
/*********
@@ -64,7 +65,7 @@ namespace StardewModdingAPI.Web.Controllers
{
this.ModDatabase = new ModToolkit().GetModDatabase(Path.Combine(environment.WebRootPath, "StardewModdingAPI.metadata.json"));
ModUpdateCheckConfig config = configProvider.Value;
- this.WikiCompatibilityPageUrl = config.WikiCompatibilityPageUrl;
+ this.CompatibilityPageUrl = config.CompatibilityPageUrl;
this.Cache = cache;
this.SuccessCacheMinutes = config.SuccessCacheMinutes;
@@ -73,11 +74,11 @@ namespace StardewModdingAPI.Web.Controllers
this.Repositories =
new IModRepository[]
{
- new ChucklefishRepository(config.ChucklefishKey, chucklefish),
- new GitHubRepository(config.GitHubKey, github),
- new NexusRepository(config.NexusKey, nexus)
+ new ChucklefishRepository(chucklefish),
+ new GitHubRepository(github),
+ new NexusRepository(nexus)
}
- .ToDictionary(p => p.VendorKey, StringComparer.CurrentCultureIgnoreCase);
+ .ToDictionary(p => p.VendorKey);
}
/// Fetch version metadata for the given mods.
@@ -89,7 +90,7 @@ namespace StardewModdingAPI.Web.Controllers
return new ModEntryModel[0];
// fetch wiki data
- WikiCompatibilityEntry[] wikiData = await this.GetWikiDataAsync();
+ WikiModEntry[] wikiData = await this.GetWikiDataAsync();
IDictionary mods = new Dictionary(StringComparer.CurrentCultureIgnoreCase);
foreach (ModSearchEntryModel mod in model.Mods)
{
@@ -113,17 +114,12 @@ namespace StardewModdingAPI.Web.Controllers
/// The wiki data.
/// Whether to include extended metadata for each mod.
/// Returns the mod data if found, else null.
- private async Task GetModData(ModSearchEntryModel search, WikiCompatibilityEntry[] wikiData, bool includeExtendedMetadata)
+ private async Task GetModData(ModSearchEntryModel search, WikiModEntry[] wikiData, bool includeExtendedMetadata)
{
- // resolve update keys
- var updateKeys = new HashSet(search.UpdateKeys ?? new string[0], StringComparer.InvariantCultureIgnoreCase);
+ // crossreference data
ModDataRecord record = this.ModDatabase.Get(search.ID);
- if (record?.Fields != null)
- {
- string defaultUpdateKey = record.Fields.FirstOrDefault(p => p.Key == ModDataFieldKey.UpdateKey && p.IsDefault)?.Value;
- if (!string.IsNullOrWhiteSpace(defaultUpdateKey))
- updateKeys.Add(defaultUpdateKey);
- }
+ WikiModEntry wikiEntry = wikiData.FirstOrDefault(entry => entry.ID.Contains(search.ID.Trim(), StringComparer.InvariantCultureIgnoreCase));
+ string[] updateKeys = this.GetUpdateKeys(search.UpdateKeys, record, wikiEntry).ToArray();
// get latest versions
ModEntryModel result = new ModEntryModel { ID = search.ID };
@@ -166,9 +162,25 @@ namespace StardewModdingAPI.Web.Controllers
}
// get unofficial version
- WikiCompatibilityEntry wikiEntry = wikiData.FirstOrDefault(entry => entry.ID.Contains(result.ID.Trim(), StringComparer.InvariantCultureIgnoreCase));
- if (wikiEntry?.UnofficialVersion != null && this.IsNewer(wikiEntry.UnofficialVersion, result.Main?.Version) && this.IsNewer(wikiEntry.UnofficialVersion, result.Optional?.Version))
- result.Unofficial = new ModEntryVersionModel(wikiEntry.UnofficialVersion, this.WikiCompatibilityPageUrl);
+ if (wikiEntry?.Compatibility.UnofficialVersion != null && this.IsNewer(wikiEntry.Compatibility.UnofficialVersion, result.Main?.Version) && this.IsNewer(wikiEntry.Compatibility.UnofficialVersion, result.Optional?.Version))
+ result.Unofficial = new ModEntryVersionModel(wikiEntry.Compatibility.UnofficialVersion, $"{this.CompatibilityPageUrl}/#{wikiEntry.Anchor}");
+
+ // get unofficial version for beta
+ if (wikiEntry?.HasBetaInfo == true)
+ {
+ result.HasBetaInfo = true;
+ if (wikiEntry.BetaCompatibility.Status == WikiCompatibilityStatus.Unofficial)
+ {
+ if (wikiEntry.BetaCompatibility.UnofficialVersion != null)
+ {
+ result.UnofficialForBeta = (wikiEntry.BetaCompatibility.UnofficialVersion != null && this.IsNewer(wikiEntry.BetaCompatibility.UnofficialVersion, result.Main?.Version) && this.IsNewer(wikiEntry.BetaCompatibility.UnofficialVersion, result.Optional?.Version))
+ ? new ModEntryVersionModel(wikiEntry.BetaCompatibility.UnofficialVersion, $"{this.CompatibilityPageUrl}/#{wikiEntry.Anchor}")
+ : null;
+ }
+ else
+ result.UnofficialForBeta = result.Unofficial;
+ }
+ }
// fallback to preview if latest is invalid
if (result.Main == null && result.Optional != null)
@@ -195,28 +207,6 @@ namespace StardewModdingAPI.Web.Controllers
return result;
}
- /// 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].Trim();
- modID = parts[1].Trim();
- return true;
- }
-
/// Get whether a version is newer than an version.
/// The current version.
/// The other version.
@@ -226,21 +216,21 @@ namespace StardewModdingAPI.Web.Controllers
}
/// Get mod data from the wiki compatibility list.
- private async Task GetWikiDataAsync()
+ private async Task GetWikiDataAsync()
{
ModToolkit toolkit = new ModToolkit();
- return await this.Cache.GetOrCreateAsync($"_wiki", async entry =>
+ return await this.Cache.GetOrCreateAsync("_wiki", async entry =>
{
try
{
- WikiCompatibilityEntry[] entries = await toolkit.GetWikiCompatibilityListAsync();
+ WikiModEntry[] entries = (await toolkit.GetWikiCompatibilityListAsync()).Mods;
entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(this.SuccessCacheMinutes);
return entries;
}
catch
{
entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(this.ErrorCacheMinutes);
- return new WikiCompatibilityEntry[0];
+ return new WikiModEntry[0];
}
});
}
@@ -250,18 +240,19 @@ namespace StardewModdingAPI.Web.Controllers
private async Task GetInfoForUpdateKeyAsync(string updateKey)
{
// parse update key
- if (!this.TryParseModKey(updateKey, out string vendorKey, out string modID))
+ UpdateKey parsed = UpdateKey.Parse(updateKey);
+ if (!parsed.LooksValid)
return new ModInfoModel($"The update key '{updateKey}' isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541'.");
// get matching repository
- if (!this.Repositories.TryGetValue(vendorKey, out IModRepository repository))
- return new ModInfoModel($"There's no mod site with key '{vendorKey}'. Expected one of [{string.Join(", ", this.Repositories.Keys)}].");
+ if (!this.Repositories.TryGetValue(parsed.Repository, out IModRepository repository))
+ return new ModInfoModel($"There's no mod site with key '{parsed.Repository}'. Expected one of [{string.Join(", ", this.Repositories.Keys)}].");
// fetch mod info
- return await this.Cache.GetOrCreateAsync($"{repository.VendorKey}:{modID}".ToLower(), async entry =>
+ return await this.Cache.GetOrCreateAsync($"{repository.VendorKey}:{parsed.ID}".ToLower(), async entry =>
{
- ModInfoModel result = await repository.GetModInfoAsync(modID);
- if (result.Error != null)
+ ModInfoModel result = await repository.GetModInfoAsync(parsed.ID);
+ if (result.Error == null)
{
if (result.Version == null)
result.Error = $"The update key '{updateKey}' matches a mod with no version number.";
@@ -273,11 +264,42 @@ namespace StardewModdingAPI.Web.Controllers
});
}
- /// Get the requested API version.
- private ISemanticVersion GetApiVersion()
+ /// Get update keys based on the available mod metadata, while maintaining the precedence order.
+ /// The specified update keys.
+ /// The mod's entry in SMAPI's internal database.
+ /// The mod's entry in the wiki list.
+ public IEnumerable GetUpdateKeys(string[] specifiedKeys, ModDataRecord record, WikiModEntry entry)
{
- string actualVersion = (string)this.RouteData.Values["version"];
- return new SemanticVersion(actualVersion);
+ IEnumerable GetRaw()
+ {
+ // specified update keys
+ if (specifiedKeys != null)
+ {
+ foreach (string key in specifiedKeys)
+ yield return key?.Trim();
+ }
+
+ // default update key
+ string defaultKey = record?.GetDefaultUpdateKey();
+ if (defaultKey != null)
+ yield return defaultKey;
+
+ // wiki metadata
+ if (entry != null)
+ {
+ if (entry.NexusID.HasValue)
+ yield return $"Nexus:{entry.NexusID}";
+ if (entry.ChucklefishID.HasValue)
+ yield return $"Chucklefish:{entry.ChucklefishID}";
+ }
+ }
+
+ HashSet seen = new HashSet(StringComparer.InvariantCulture);
+ foreach (string key in GetRaw())
+ {
+ if (!string.IsNullOrWhiteSpace(key) && seen.Add(key))
+ yield return key;
+ }
}
}
}
diff --git a/src/SMAPI.Web/Controllers/ModsController.cs b/src/SMAPI.Web/Controllers/ModsController.cs
new file mode 100644
index 00000000..57aa9da9
--- /dev/null
+++ b/src/SMAPI.Web/Controllers/ModsController.cs
@@ -0,0 +1,74 @@
+using System;
+using System.Linq;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Caching.Memory;
+using Microsoft.Extensions.Options;
+using StardewModdingAPI.Toolkit;
+using StardewModdingAPI.Toolkit.Framework.Clients.Wiki;
+using StardewModdingAPI.Web.Framework.ConfigModels;
+using StardewModdingAPI.Web.ViewModels;
+
+namespace StardewModdingAPI.Web.Controllers
+{
+ /// Provides user-friendly info about SMAPI mods.
+ internal class ModsController : Controller
+ {
+ /*********
+ ** Properties
+ *********/
+ /// The cache in which to store mod metadata.
+ private readonly IMemoryCache Cache;
+
+ /// The number of minutes successful update checks should be cached before refetching them.
+ private readonly int SuccessCacheMinutes;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The cache in which to store mod metadata.
+ /// The config settings for mod update checks.
+ public ModsController(IMemoryCache cache, IOptions configProvider)
+ {
+ ModUpdateCheckConfig config = configProvider.Value;
+
+ this.Cache = cache;
+ this.SuccessCacheMinutes = config.SuccessCacheMinutes;
+ }
+
+ /// Display information for all mods.
+ [HttpGet]
+ [Route("mods")]
+ public async Task Index()
+ {
+ return this.View("Index", await this.FetchDataAsync());
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// Asynchronously fetch mod metadata from the wiki.
+ public async Task FetchDataAsync()
+ {
+ return await this.Cache.GetOrCreateAsync($"{nameof(ModsController)}_mod_list", async entry =>
+ {
+ WikiModList data = await new ModToolkit().GetWikiCompatibilityListAsync();
+ ModListModel model = new ModListModel(
+ stableVersion: data.StableVersion,
+ betaVersion: data.BetaVersion,
+ mods: data
+ .Mods
+ .Select(mod => new ModModel(mod))
+ .OrderBy(p => Regex.Replace(p.Name.ToLower(), "[^a-z0-9]", "")) // ignore case, spaces, and special characters when sorting
+ );
+
+ entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(this.SuccessCacheMinutes);
+ return model;
+ });
+ }
+ }
+}
diff --git a/src/SMAPI.Web/Framework/Clients/Nexus/NexusMod.cs b/src/SMAPI.Web/Framework/Clients/Nexus/NexusMod.cs
index 4ecf2f76..f4909155 100644
--- a/src/SMAPI.Web/Framework/Clients/Nexus/NexusMod.cs
+++ b/src/SMAPI.Web/Framework/Clients/Nexus/NexusMod.cs
@@ -1,5 +1,4 @@
using Newtonsoft.Json;
-using StardewModdingAPI.Toolkit;
namespace StardewModdingAPI.Web.Framework.Clients.Nexus
{
diff --git a/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs
index ce4f3cb5..bde566c0 100644
--- a/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs
+++ b/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs
@@ -16,16 +16,7 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels
/// Derived from SMAPI's SemanticVersion implementation.
public string SemanticVersionRegex { get; set; }
- /// The repository key for the Chucklefish mod site.
- public string ChucklefishKey { get; set; }
-
- /// The repository key for Nexus Mods.
- public string GitHubKey { get; set; }
-
- /// The repository key for Nexus Mods.
- public string NexusKey { get; set; }
-
/// The web URL for the wiki compatibility list.
- public string WikiCompatibilityPageUrl { get; set; }
+ public string CompatibilityPageUrl { get; set; }
}
}
diff --git a/src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs
index 3d428015..d89a4260 100644
--- a/src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs
+++ b/src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs
@@ -12,7 +12,13 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels
/// The root URL for the log parser.
public string LogParserUrl { get; set; }
+ /// The root URL for the mod list.
+ public string ModListUrl { get; set; }
+
/// Whether to show SMAPI beta versions on the main page, if any.
- public bool EnableSmapiBeta { get; set; }
+ public bool BetaEnabled { get; set; }
+
+ /// A short sentence shown under the beta download button, if any.
+ public string BetaBlurb { get; set; }
}
}
diff --git a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs
index 013c6c47..f9b5ba76 100644
--- a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs
+++ b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs
@@ -70,6 +70,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
// parse log messages
LogModInfo smapiMod = new LogModInfo { Name = "SMAPI", Author = "Pathoschild", Description = "" };
+ LogModInfo gameMod = new LogModInfo { Name = "game", Author = "", Description = "" };
IDictionary mods = new Dictionary();
bool inModList = false;
bool inContentPackList = false;
@@ -78,10 +79,23 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
// collect stats
if (message.Level == LogLevel.Error)
{
- if (message.Mod == "SMAPI")
- smapiMod.Errors++;
- else if (mods.ContainsKey(message.Mod))
- mods[message.Mod].Errors++;
+ switch (message.Mod)
+ {
+ case "SMAPI":
+ smapiMod.Errors++;
+ break;
+
+ case "game":
+ gameMod.Errors++;
+ break;
+
+ default:
+ {
+ if (mods.ContainsKey(message.Mod))
+ mods[message.Mod].Errors++;
+ break;
+ }
+ }
}
// collect SMAPI metadata
@@ -151,7 +165,8 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
}
// finalise log
- log.Mods = new[] { smapiMod }.Concat(mods.Values.OrderBy(p => p.Name)).ToArray();
+ gameMod.Version = log.GameVersion;
+ log.Mods = new[] { gameMod, smapiMod }.Concat(mods.Values.OrderBy(p => p.Name)).ToArray();
return log;
}
catch (LogParseException ex)
diff --git a/src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs
index 4a4a40cd..94256005 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.Toolkit.Framework.Clients.WebApi;
+using StardewModdingAPI.Toolkit.Framework.UpdateData;
namespace StardewModdingAPI.Web.Framework.ModRepositories
{
@@ -10,7 +10,7 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
** Accessors
*********/
/// The unique key for this vendor.
- public string VendorKey { get; }
+ public ModRepositoryKey VendorKey { get; }
/*********
@@ -29,7 +29,7 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
*********/
/// Construct an instance.
/// The unique key for this vendor.
- protected RepositoryBase(string vendorKey)
+ protected RepositoryBase(ModRepositoryKey vendorKey)
{
this.VendorKey = vendorKey;
}
diff --git a/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs
index e6074a60..6e2a8814 100644
--- a/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs
+++ b/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs
@@ -1,6 +1,6 @@
using System;
using System.Threading.Tasks;
-using StardewModdingAPI.Toolkit.Framework.Clients.WebApi;
+using StardewModdingAPI.Toolkit.Framework.UpdateData;
using StardewModdingAPI.Web.Framework.Clients.Chucklefish;
namespace StardewModdingAPI.Web.Framework.ModRepositories
@@ -19,10 +19,9 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
** Public methods
*********/
/// Construct an instance.
- /// The unique key for this vendor.
/// The underlying HTTP client.
- public ChucklefishRepository(string vendorKey, IChucklefishClient client)
- : base(vendorKey)
+ public ChucklefishRepository(IChucklefishClient client)
+ : base(ModRepositoryKey.Chucklefish)
{
this.Client = client;
}
diff --git a/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs
index 1d7e4fff..7ff22d0e 100644
--- a/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs
+++ b/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs
@@ -1,6 +1,6 @@
using System;
using System.Threading.Tasks;
-using StardewModdingAPI.Toolkit.Framework.Clients.WebApi;
+using StardewModdingAPI.Toolkit.Framework.UpdateData;
using StardewModdingAPI.Web.Framework.Clients.GitHub;
namespace StardewModdingAPI.Web.Framework.ModRepositories
@@ -19,10 +19,9 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
** Public methods
*********/
/// Construct an instance.
- /// The unique key for this vendor.
/// The underlying GitHub API client.
- public GitHubRepository(string vendorKey, IGitHubClient client)
- : base(vendorKey)
+ public GitHubRepository(IGitHubClient client)
+ : base(ModRepositoryKey.GitHub)
{
this.Client = client;
}
diff --git a/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs
index 09c59a86..68f754ae 100644
--- a/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs
+++ b/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs
@@ -1,5 +1,6 @@
using System;
using System.Threading.Tasks;
+using StardewModdingAPI.Toolkit.Framework.UpdateData;
namespace StardewModdingAPI.Web.Framework.ModRepositories
{
@@ -10,7 +11,7 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
** Accessors
*********/
/// The unique key for this vendor.
- string VendorKey { get; }
+ ModRepositoryKey VendorKey { get; }
/*********
diff --git a/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs
index 4afcda10..1e242c60 100644
--- a/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs
+++ b/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs
@@ -1,6 +1,6 @@
using System;
using System.Threading.Tasks;
-using StardewModdingAPI.Toolkit.Framework.Clients.WebApi;
+using StardewModdingAPI.Toolkit.Framework.UpdateData;
using StardewModdingAPI.Web.Framework.Clients.Nexus;
namespace StardewModdingAPI.Web.Framework.ModRepositories
@@ -19,10 +19,9 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
** Public methods
*********/
/// Construct an instance.
- /// The unique key for this vendor.
/// The underlying Nexus Mods API client.
- public NexusRepository(string vendorKey, INexusClient client)
- : base(vendorKey)
+ public NexusRepository(INexusClient client)
+ : base(ModRepositoryKey.Nexus)
{
this.Client = client;
}
diff --git a/src/SMAPI.Web/StardewModdingAPI.Web.csproj b/src/SMAPI.Web/StardewModdingAPI.Web.csproj
index 6761c7ad..9d1990d9 100644
--- a/src/SMAPI.Web/StardewModdingAPI.Web.csproj
+++ b/src/SMAPI.Web/StardewModdingAPI.Web.csproj
@@ -10,10 +10,10 @@
-
-
-
-
+
+
+
+
@@ -27,6 +27,12 @@
+
+ $(IncludeRazorContentInPack)
+
+
+ $(IncludeRazorContentInPack)
+ PreserveNewest
diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs
index bf3ec9a1..60a16053 100644
--- a/src/SMAPI.Web/Startup.cs
+++ b/src/SMAPI.Web/Startup.cs
@@ -147,14 +147,14 @@ namespace StardewModdingAPI.Web
redirects.Add(new ConditionalRewriteSubdomainRule(
shouldRewrite: req =>
req.Host.Host != "localhost"
- && (req.Host.Host.StartsWith("api.") || req.Host.Host.StartsWith("log."))
+ && (req.Host.Host.StartsWith("api.") || req.Host.Host.StartsWith("log.") || req.Host.Host.StartsWith("mods."))
&& !req.Path.StartsWithSegments("/content")
&& !req.Path.StartsWithSegments("/favicon.ico")
));
// shortcut redirects
redirects.Add(new RedirectToUrlRule(@"^/buildmsg(?:/?(.*))$", "https://github.com/Pathoschild/SMAPI/blob/develop/docs/mod-build-config.md#$1"));
- redirects.Add(new RedirectToUrlRule(@"^/compat\.?$", "https://stardewvalleywiki.com/Modding:SMAPI_compatibility"));
+ redirects.Add(new RedirectToUrlRule(@"^/compat\.?$", "https://mods.smapi.io"));
redirects.Add(new RedirectToUrlRule(@"^/docs\.?$", "https://stardewvalleywiki.com/Modding:Index"));
redirects.Add(new RedirectToUrlRule(@"^/install\.?$", "https://stardewvalleywiki.com/Modding:Player_Guide/Getting_Started#Install_SMAPI"));
diff --git a/src/SMAPI.Web/ViewModels/IndexModel.cs b/src/SMAPI.Web/ViewModels/IndexModel.cs
index 4268c878..82c4e06f 100644
--- a/src/SMAPI.Web/ViewModels/IndexModel.cs
+++ b/src/SMAPI.Web/ViewModels/IndexModel.cs
@@ -12,6 +12,9 @@ namespace StardewModdingAPI.Web.ViewModels
/// The latest prerelease SMAPI version (if newer than ).
public IndexVersionModel BetaVersion { get; set; }
+ /// A short sentence shown under the beta download button, if any.
+ public string BetaBlurb { get; set; }
+
/*********
** Public methods
@@ -22,10 +25,12 @@ namespace StardewModdingAPI.Web.ViewModels
/// Construct an instance.
/// The latest stable SMAPI version.
/// The latest prerelease SMAPI version (if newer than ).
- internal IndexModel(IndexVersionModel stableVersion, IndexVersionModel betaVersion)
+ /// A short sentence shown under the beta download button, if any.
+ internal IndexModel(IndexVersionModel stableVersion, IndexVersionModel betaVersion, string betaBlurb)
{
this.StableVersion = stableVersion;
this.BetaVersion = betaVersion;
+ this.BetaBlurb = betaBlurb;
}
}
}
diff --git a/src/SMAPI.Web/ViewModels/ModCompatibilityModel.cs b/src/SMAPI.Web/ViewModels/ModCompatibilityModel.cs
new file mode 100644
index 00000000..85bf1e46
--- /dev/null
+++ b/src/SMAPI.Web/ViewModels/ModCompatibilityModel.cs
@@ -0,0 +1,40 @@
+using StardewModdingAPI.Toolkit.Framework.Clients.Wiki;
+
+namespace StardewModdingAPI.Web.ViewModels
+{
+ /// Metadata about a mod's compatibility with the latest versions of SMAPI and Stardew Valley.
+ public class ModCompatibilityModel
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// The compatibility status, as a string like "Broken".
+ public string Status { get; set; }
+
+ /// The human-readable summary, as an HTML block.
+ public string Summary { get; set; }
+
+ /// The game or SMAPI version which broke this mod (if applicable).
+ public string BrokeIn { get; set; }
+
+ /// A link to the unofficial version which fixes compatibility, if any.
+ public ModLinkModel UnofficialVersion { get; set; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The mod metadata.
+ public ModCompatibilityModel(WikiCompatibilityInfo info)
+ {
+ this.Status = info.Status.ToString();
+ this.Status = this.Status.Substring(0, 1).ToLower() + this.Status.Substring(1);
+
+ this.Summary = info.Summary;
+ this.BrokeIn = info.BrokeIn;
+ if (info.UnofficialVersion != null)
+ this.UnofficialVersion = new ModLinkModel(info.UnofficialUrl, info.UnofficialVersion.ToString());
+ }
+ }
+}
diff --git a/src/SMAPI.Web/ViewModels/ModLinkModel.cs b/src/SMAPI.Web/ViewModels/ModLinkModel.cs
new file mode 100644
index 00000000..97dd215c
--- /dev/null
+++ b/src/SMAPI.Web/ViewModels/ModLinkModel.cs
@@ -0,0 +1,28 @@
+namespace StardewModdingAPI.Web.ViewModels
+{
+ /// Metadata about a link.
+ public class ModLinkModel
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// The URL of the linked page.
+ public string Url { get; set; }
+
+ /// The suggested link text.
+ public string Text { get; set; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The URL of the linked page.
+ /// The suggested link text.
+ public ModLinkModel(string url, string text)
+ {
+ this.Url = url;
+ this.Text = text;
+ }
+ }
+}
diff --git a/src/SMAPI.Web/ViewModels/ModListModel.cs b/src/SMAPI.Web/ViewModels/ModListModel.cs
new file mode 100644
index 00000000..3b87d393
--- /dev/null
+++ b/src/SMAPI.Web/ViewModels/ModListModel.cs
@@ -0,0 +1,36 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace StardewModdingAPI.Web.ViewModels
+{
+ /// Metadata for the mod list page.
+ public class ModListModel
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// The current stable version of the game.
+ public string StableVersion { get; set; }
+
+ /// The current beta version of the game (if any).
+ public string BetaVersion { get; set; }
+
+ /// The mods to display.
+ public ModModel[] Mods { get; set; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The current stable version of the game.
+ /// The current beta version of the game (if any).
+ /// The mods to display.
+ public ModListModel(string stableVersion, string betaVersion, IEnumerable mods)
+ {
+ this.StableVersion = stableVersion;
+ this.BetaVersion = betaVersion;
+ this.Mods = mods.ToArray();
+ }
+ }
+}
diff --git a/src/SMAPI.Web/ViewModels/ModModel.cs b/src/SMAPI.Web/ViewModels/ModModel.cs
new file mode 100644
index 00000000..0e7d2076
--- /dev/null
+++ b/src/SMAPI.Web/ViewModels/ModModel.cs
@@ -0,0 +1,110 @@
+using System.Collections.Generic;
+using System.Linq;
+using StardewModdingAPI.Toolkit.Framework.Clients.Wiki;
+
+namespace StardewModdingAPI.Web.ViewModels
+{
+ /// Metadata about a mod.
+ public class ModModel
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// The mod name.
+ public string Name { get; set; }
+
+ /// The mod's alternative names, if any.
+ public string AlternateNames { get; set; }
+
+ /// The mod author's name.
+ public string Author { get; set; }
+
+ /// The mod author's alternative names, if any.
+ public string AlternateAuthors { get; set; }
+
+ /// The URL to the mod's source code, if any.
+ public string SourceUrl { get; set; }
+
+ /// The compatibility status for the stable version of the game.
+ public ModCompatibilityModel Compatibility { get; set; }
+
+ /// The compatibility status for the beta version of the game.
+ public ModCompatibilityModel BetaCompatibility { get; set; }
+
+ /// Links to the available mod pages.
+ public ModLinkModel[] ModPages { get; set; }
+
+ /// The human-readable warnings for players about this mod.
+ public string[] Warnings { get; set; }
+
+ /// A unique identifier for the mod that can be used in an anchor URL.
+ public string Slug { get; set; }
+
+ /// The sites where the mod can be downloaded.
+ public string[] ModPageSites => this.ModPages.Select(p => p.Text).ToArray();
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The mod metadata.
+ public ModModel(WikiModEntry entry)
+ {
+ // basic info
+ this.Name = entry.Name.FirstOrDefault();
+ this.AlternateNames = string.Join(", ", entry.Name.Skip(1).ToArray());
+ this.Author = entry.Author.FirstOrDefault();
+ this.AlternateAuthors = string.Join(", ", entry.Author.Skip(1).ToArray());
+ this.SourceUrl = this.GetSourceUrl(entry);
+ this.Compatibility = new ModCompatibilityModel(entry.Compatibility);
+ this.BetaCompatibility = entry.BetaCompatibility != null ? new ModCompatibilityModel(entry.BetaCompatibility) : null;
+ this.ModPages = this.GetModPageUrls(entry).ToArray();
+ this.Warnings = entry.Warnings;
+ this.Slug = entry.Anchor;
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// Get the web URL for the mod's source code repository, if any.
+ /// The mod metadata.
+ private string GetSourceUrl(WikiModEntry entry)
+ {
+ if (!string.IsNullOrWhiteSpace(entry.GitHubRepo))
+ return $"https://github.com/{entry.GitHubRepo}";
+ if (!string.IsNullOrWhiteSpace(entry.CustomSourceUrl))
+ return entry.CustomSourceUrl;
+ return null;
+ }
+
+ /// Get the web URLs for the mod pages, if any.
+ /// The mod metadata.
+ private IEnumerable GetModPageUrls(WikiModEntry entry)
+ {
+ bool anyFound = false;
+
+ // normal mod pages
+ if (entry.NexusID.HasValue)
+ {
+ anyFound = true;
+ yield return new ModLinkModel($"https://www.nexusmods.com/stardewvalley/mods/{entry.NexusID}", "Nexus");
+ }
+ if (entry.ChucklefishID.HasValue)
+ {
+ anyFound = true;
+ yield return new ModLinkModel($"https://community.playstarbound.com/resources/{entry.ChucklefishID}", "Chucklefish");
+ }
+
+ // fallback
+ if (!anyFound && !string.IsNullOrWhiteSpace(entry.CustomUrl))
+ {
+ anyFound = true;
+ yield return new ModLinkModel(entry.CustomUrl, "custom");
+ }
+ if (!anyFound && !string.IsNullOrWhiteSpace(entry.GitHubRepo))
+ yield return new ModLinkModel($"https://github.com/{entry.GitHubRepo}/releases", "GitHub");
+ }
+ }
+}
diff --git a/src/SMAPI.Web/Views/Index/Index.cshtml b/src/SMAPI.Web/Views/Index/Index.cshtml
index 361d01de..01874f50 100644
--- a/src/SMAPI.Web/Views/Index/Index.cshtml
+++ b/src/SMAPI.Web/Views/Index/Index.cshtml
@@ -1,10 +1,13 @@
+@using Microsoft.Extensions.Options
+@using StardewModdingAPI.Web.Framework.ConfigModels
+@inject IOptions SiteConfig
+@model StardewModdingAPI.Web.ViewModels.IndexModel
@{
ViewData["Title"] = "SMAPI";
}
-@model StardewModdingAPI.Web.ViewModels.IndexModel
@section Head {
-
+
}
@@ -16,7 +19,7 @@
Special thanks to
+ AbroadKew,
acerbicon,
ChefRude,
cheesysteak,
hawkfalcon,
jwdred,
- KNakamura,
- Kono Tyran,
+ Karmylla,
Pucklynn,
Robby LaFarge,
and a few anonymous users for their ongoing support; you're awesome! 🏅
diff --git a/src/SMAPI.Web/Views/Index/Privacy.cshtml b/src/SMAPI.Web/Views/Index/Privacy.cshtml
new file mode 100644
index 00000000..ca99eef6
--- /dev/null
+++ b/src/SMAPI.Web/Views/Index/Privacy.cshtml
@@ -0,0 +1,43 @@
+@using Microsoft.Extensions.Options
+@using StardewModdingAPI.Web.Framework.ConfigModels
+@inject IOptions SiteConfig
+@{
+ ViewData["Title"] = "SMAPI privacy notes";
+}
+@section Head {
+
+}
+
+← back to SMAPI page
+
+
SMAPI is an open-source and non-profit project. Your privacy is important, so this page explains what information SMAPI uses and transmits. This page is informational only, it's not a legal document.
+
+
Principles
+
+
SMAPI collects the minimum information needed to enable its features (see below).
+
SMAPI does not collect telemetry, analytics, etc.
+
SMAPI will never sell your information.
+
+
+
Data collected and transmitted
+
Web logging
+
This website and SMAPI's web API are hosted by Amazon Web Services. Their servers may automatically collect diagnostics like your IP address, but this information is not visible to SMAPI's web application or developers. For more information, see the Amazon Privacy Notice.
+
+
Update checks
+
SMAPI notifies you when there's a new version of SMAPI or your mods available. To do so, it sends your SMAPI and mod versions to its web API. No personal information is stored by the web application, but see web logging.
+
+
You can disable update checks, and no information will be transmitted to the web API. To do so:
open the smapi-internal/StardewModdingAPI.config.json file in a text editor;
+
change "CheckForUpdates": true to "CheckForUpdates": false.
+
+
+
Log parser
+
The log parser page lets you store a log file for analysis and sharing. The log data is stored indefinitely in an obfuscated form as unlisted pastes in Pastebin. No personal information is stored by the log parser beyond what you choose to upload, but see web logging and the Pastebin Privacy Statement.
+
+
Multiplayer sync
+
As part of its multiplayer API, SMAPI transmits basic context to players you connect to (mainly your OS, SMAPI version, game version, and installed mods). This is used to enable multiplayer features like inter-mod messages, compatibility checks, etc. Although this information is normally hidden from players, it may be visible due to mods or configuration changes.
+
+
Custom mods
+
Mods may collect and transmit any information. Mods (except those provided as part of the SMAPI download) are not covered by this page. Install third-party mods at your own risk.
This page lists all known SMAPI mods, whether they're compatible with the latest versions of Stardew Valley and SMAPI, and how to fix broken mods if possible. The list is updated every few days. (You can help edit this list!)
Note: "SDV beta only" means Stardew Valley @Model.BetaVersion-beta; if you didn't opt in to the beta, you have the stable version and can ignore that line. If a mod doesn't have a "SDV beta only" line, the compatibility applies to both versions of the game.