From 304d89cd193671942fd96b2eaca31ab18f9304f1 Mon Sep 17 00:00:00 2001 From: Kris Scott Date: Mon, 22 Apr 2019 17:37:01 +0900 Subject: Reworked the unix launcher - More posix compliance and less prone to failing on bad variables - Replaced depreciated backquotes (`) with the 'which' command - Reworked the entire terminal detection code for easier editing and testing --- src/SMAPI.Installer/unix-launcher.sh | 95 +++++++++++++++++++++++------------- 1 file changed, 60 insertions(+), 35 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Installer/unix-launcher.sh b/src/SMAPI.Installer/unix-launcher.sh index a98d527d..d6c3a004 100644 --- a/src/SMAPI.Installer/unix-launcher.sh +++ b/src/SMAPI.Installer/unix-launcher.sh @@ -1,14 +1,14 @@ -#!/bin/bash +#!/usr/bin/env bash # MonoKickstart Shell Script # Written by Ethan "flibitijibibo" Lee # Modified for StardewModdingAPI by Viz and Pathoschild # Move to script's directory -cd "`dirname "$0"`" +cd "$(dirname "$0")" || exit $? # Get the system architecture -UNAME=`uname` -ARCH=`uname -m` +UNAME=$(uname) +ARCH=$(uname -m) # MonoKickstart picks the right libfolder, so just execute the right binary. if [ "$UNAME" == "Darwin" ]; then @@ -39,18 +39,18 @@ if [ "$UNAME" == "Darwin" ]; then # launch SMAPI cp StardewValley.bin.osx StardewModdingAPI.bin.osx - open -a Terminal ./StardewModdingAPI.bin.osx $@ + open -a Terminal ./StardewModdingAPI.bin.osx "$@" else # choose launcher LAUNCHER="" if [ "$ARCH" == "x86_64" ]; then ln -sf mcs.bin.x86_64 mcs cp StardewValley.bin.x86_64 StardewModdingAPI.bin.x86_64 - LAUNCHER="./StardewModdingAPI.bin.x86_64 $@" + LAUNCHER="./StardewModdingAPI.bin.x86_64 $*" else ln -sf mcs.bin.x86 mcs cp StardewValley.bin.x86 StardewModdingAPI.bin.x86 - LAUNCHER="./StardewModdingAPI.bin.x86 $@" + LAUNCHER="./StardewModdingAPI.bin.x86 $*" fi # get cross-distro version of POSIX command @@ -62,36 +62,61 @@ else fi # open SMAPI in terminal - 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 "sh -c 'TERM=xterm $LAUNCHER'" - else - x-terminal-emulator -e "sh -c 'TERM=xterm $LAUNCHER'" + # First let's try xterm (best compatiblity) or find a sensible default + # Setting TERMINAL should let you override this at the commandline for easier testing of other terminals. + for terminal in "$TERMINAL" xterm x-terminal-emulator kitty terminator xfce4-terminal gnome-terminal konsole terminal termite; do + if $COMMAND "$terminal" 2>/dev/null; then + # Find the true shell behind x-terminal-emulator + if [ "$(basename "$(readlink -ef which "$terminal")")" != "x-terminal-emulator" ]; then + export LAUNCHTERM=$terminal + break; + else + export LAUNCHTERM="$(basename "$(readlink -ef which x-terminal-emulator)")" + # Remember that we are using x-terminal-emulator just in case it points outside the $PATH + export XTE=1 + break; + fi fi - elif $COMMAND xfce4-terminal 2>/dev/null; then - xfce4-terminal -e "sh -c 'TERM=xterm $LAUNCHER'" - elif $COMMAND gnome-terminal 2>/dev/null; then - 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 "sh -c 'TERM=xterm $LAUNCHER'" - elif $COMMAND termite 2>/dev/null; then - termite -e "sh -c 'TERM=xterm $LAUNCHER'" - else + done + + if [ -z "$LAUNCHTERM" ]; then + # We failed to detect a terminal, run in current shell, or with no output sh -c 'TERM=xterm $LAUNCHER' + if [ $? -eq 127 ]; then + $LAUNCHER --no-terminal + fi + exit fi - # some Linux users get error 127 (command not found) from the above block, even though - # `command -v` indicates the command is valid. As a fallback, launch SMAPI without a terminal when - # that happens and pass in an argument indicating SMAPI shouldn't try writing to the terminal - # (which can be slow if there is none). - if [ $? -eq 127 ]; then - $LAUNCHER --no-terminal - fi + # Now let's run the terminal and account for quirks, or if no terminal was found, run in the current process. + case $LAUNCHTERM in + terminator) + # Terminator converts -e to -x when used through x-terminal-emulator for some reason + if $XTE; then + terminator -e "sh -c 'TERM=xterm $LAUNCHER'" + else + terminator -x "sh -c 'TERM=xterm $LAUNCHER'" + fi + ;; + kitty) + # Kitty overrides the TERM varible unless you set it explicitly + kitty -o term=xterm $LAUNCHER + ;; + xterm) + $LAUNCHTERM -e "$LAUNCHER" + ;; + xfce4-terminal|gnome-terminal|terminal|termite) + $LAUNCHTERM -e "sh -c 'TERM=xterm $LAUNCHER'" + ;; + konsole) + konsole -p Environment=TERM=xterm -e "$LAUNCHER" + ;; + *) + # If we don't know the terminal, just try to run it in the current shell. + sh -c 'TERM=xterm $LAUNCHER' + # if THAT fails, launch with no output + if [ $? -eq 127 ]; then + $LAUNCHER --no-terminal + fi + esac fi -- cgit From d14e470ea9e2e7533fe7fc873b213bacde282258 Mon Sep 17 00:00:00 2001 From: kurumushi Date: Mon, 22 Apr 2019 08:52:18 +0000 Subject: Update unix-launcher.sh Changed the line to launch xterm, forcing TERM=xterm, as suggested by @toastal --- src/SMAPI.Installer/unix-launcher.sh | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Installer/unix-launcher.sh b/src/SMAPI.Installer/unix-launcher.sh index d6c3a004..09b3f830 100644 --- a/src/SMAPI.Installer/unix-launcher.sh +++ b/src/SMAPI.Installer/unix-launcher.sh @@ -102,10 +102,7 @@ else # Kitty overrides the TERM varible unless you set it explicitly kitty -o term=xterm $LAUNCHER ;; - xterm) - $LAUNCHTERM -e "$LAUNCHER" - ;; - xfce4-terminal|gnome-terminal|terminal|termite) + xterm|xfce4-terminal|gnome-terminal|terminal|termite) $LAUNCHTERM -e "sh -c 'TERM=xterm $LAUNCHER'" ;; konsole) -- cgit From 7e9cb99acb143bc34f93c08086b95af0f95639ef Mon Sep 17 00:00:00 2001 From: kurumushi Date: Mon, 22 Apr 2019 10:50:19 +0000 Subject: Update unix-launcher.sh minor formatting fixes --- src/SMAPI.Installer/unix-launcher.sh | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Installer/unix-launcher.sh b/src/SMAPI.Installer/unix-launcher.sh index 09b3f830..fc08577f 100644 --- a/src/SMAPI.Installer/unix-launcher.sh +++ b/src/SMAPI.Installer/unix-launcher.sh @@ -64,17 +64,17 @@ else # open SMAPI in terminal # First let's try xterm (best compatiblity) or find a sensible default # Setting TERMINAL should let you override this at the commandline for easier testing of other terminals. - for terminal in "$TERMINAL" xterm x-terminal-emulator kitty terminator xfce4-terminal gnome-terminal konsole terminal termite; do + for terminal in "$TERMINAL" xterm x-terminal-emulator kitty terminator xfce4-terminal gnome-terminal konsole terminal termite; do if $COMMAND "$terminal" 2>/dev/null; then # Find the true shell behind x-terminal-emulator if [ "$(basename "$(readlink -ef which "$terminal")")" != "x-terminal-emulator" ]; then - export LAUNCHTERM=$terminal - break; + export LAUNCHTERM=$terminal + break; else - export LAUNCHTERM="$(basename "$(readlink -ef which x-terminal-emulator)")" - # Remember that we are using x-terminal-emulator just in case it points outside the $PATH - export XTE=1 - break; + export LAUNCHTERM="$(basename "$(readlink -ef which x-terminal-emulator)")" + # Remember that we are using x-terminal-emulator just in case it points outside the $PATH + export XTE=1 + break; fi fi done -- cgit From e55295385b592f422d43f0b525cf3bea7be19b52 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 31 Dec 2018 13:54:06 -0500 Subject: add HasFile content pack method --- docs/release-notes.md | 6 ++++++ src/SMAPI/Framework/ContentPack.cs | 27 +++++++++++++++++++++++---- src/SMAPI/IContentPack.cs | 4 ++++ 3 files changed, 33 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 5a7d5ef2..aae5cf3a 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,4 +1,10 @@ # Release notes +## 3.0 (upcoming release) +These changes have not been released yet. + +* For modders: + * Added `IContentPack.HasFile` method. + ## 2.11.3 Released 13 September 2019 for Stardew Valley 1.3.36. diff --git a/src/SMAPI/Framework/ContentPack.cs b/src/SMAPI/Framework/ContentPack.cs index e39d03a1..5384d98f 100644 --- a/src/SMAPI/Framework/ContentPack.cs +++ b/src/SMAPI/Framework/ContentPack.cs @@ -47,6 +47,15 @@ namespace StardewModdingAPI.Framework this.JsonHelper = jsonHelper; } + /// Get whether a given file exists in the content pack. + /// The file path to check. + public bool HasFile(string path) + { + this.AssertRelativePath(path, nameof(this.HasFile)); + + return File.Exists(Path.Combine(this.DirectoryPath, path)); + } + /// Read a JSON file from the content pack folder. /// The model type. /// The file path relative to the contnet directory. @@ -54,8 +63,7 @@ namespace StardewModdingAPI.Framework /// The is not relative or contains directory climbing (../). public TModel ReadJsonFile(string path) where TModel : class { - if (!PathUtilities.IsSafeRelativePath(path)) - throw new InvalidOperationException($"You must call {nameof(IContentPack)}.{nameof(this.ReadJsonFile)} with a relative path."); + this.AssertRelativePath(path, nameof(this.ReadJsonFile)); path = Path.Combine(this.DirectoryPath, PathUtilities.NormalisePathSeparators(path)); return this.JsonHelper.ReadJsonFileIfExists(path, out TModel model) @@ -70,8 +78,7 @@ namespace StardewModdingAPI.Framework /// The is not relative or contains directory climbing (../). public void WriteJsonFile(string path, TModel data) where TModel : class { - if (!PathUtilities.IsSafeRelativePath(path)) - throw new InvalidOperationException($"You must call {nameof(IContentPack)}.{nameof(this.WriteJsonFile)} with a relative path."); + this.AssertRelativePath(path, nameof(this.WriteJsonFile)); path = Path.Combine(this.DirectoryPath, PathUtilities.NormalisePathSeparators(path)); this.JsonHelper.WriteJsonFile(path, data); @@ -95,5 +102,17 @@ namespace StardewModdingAPI.Framework return this.Content.GetActualAssetKey(key, ContentSource.ModFolder); } + + /********* + ** Private methods + *********/ + /// Assert that a relative path was passed it to a content pack method. + /// The path to check. + /// The name of the method which was invoked. + private void AssertRelativePath(string path, string methodName) + { + if (!PathUtilities.IsSafeRelativePath(path)) + throw new InvalidOperationException($"You must call {nameof(IContentPack)}.{methodName} with a relative path."); + } } } diff --git a/src/SMAPI/IContentPack.cs b/src/SMAPI/IContentPack.cs index 9ba32394..32cbc6fc 100644 --- a/src/SMAPI/IContentPack.cs +++ b/src/SMAPI/IContentPack.cs @@ -21,6 +21,10 @@ namespace StardewModdingAPI /********* ** Public methods *********/ + /// Get whether a given file exists in the content pack. + /// The file path to check. + bool HasFile(string path); + /// Read a JSON file from the content pack folder. /// The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types. /// The file path relative to the content pack directory. -- cgit From 31a49b83c2fda9f3990e953aeaefc2b415333f31 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 4 Jan 2019 18:32:12 -0500 Subject: update NuGet packages --- docs/release-notes.md | 1 + .../SMAPI.ModBuildConfig.Analyzer.Tests.csproj | 2 +- src/SMAPI.Tests/StardewModdingAPI.Tests.csproj | 4 ++-- src/SMAPI.Toolkit/StardewModdingAPI.Toolkit.csproj | 4 ++-- src/SMAPI.Web/StardewModdingAPI.Web.csproj | 4 ++-- src/SMAPI/StardewModdingAPI.csproj | 2 +- 6 files changed, 9 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index aae5cf3a..efbd7ddb 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ These changes have not been released yet. * For modders: * Added `IContentPack.HasFile` method. + * Updated to Json.NET 12.0.1. ## 2.11.3 Released 13 September 2019 for Stardew Valley 1.3.36. 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 45953eec..747a1d69 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj +++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj @@ -9,7 +9,7 @@ - + diff --git a/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj b/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj index 1cb2d1e6..68cf7b34 100644 --- a/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj +++ b/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj @@ -17,8 +17,8 @@ - - + + diff --git a/src/SMAPI.Toolkit/StardewModdingAPI.Toolkit.csproj b/src/SMAPI.Toolkit/StardewModdingAPI.Toolkit.csproj index 46d38f17..a386fc5e 100644 --- a/src/SMAPI.Toolkit/StardewModdingAPI.Toolkit.csproj +++ b/src/SMAPI.Toolkit/StardewModdingAPI.Toolkit.csproj @@ -14,8 +14,8 @@ - - + + diff --git a/src/SMAPI.Web/StardewModdingAPI.Web.csproj b/src/SMAPI.Web/StardewModdingAPI.Web.csproj index d47361bd..88a13f7b 100644 --- a/src/SMAPI.Web/StardewModdingAPI.Web.csproj +++ b/src/SMAPI.Web/StardewModdingAPI.Web.csproj @@ -15,8 +15,8 @@ - - + + diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index eda53025..718366a7 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -19,7 +19,7 @@ - + -- cgit From 6fdb29d806eddd17f0ac33401781f7df58639075 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 10 Jan 2019 19:30:11 -0500 Subject: drop support for old versions of SMAPI and Visual Studio in mod build package --- build/prepare-nuget-package.targets | 2 - docs/mod-build-config.md | 3 + .../tools/install.ps1 | 58 ------------------- .../tools/uninstall.ps1 | 65 ---------------------- src/SMAPI.ModBuildConfig/build/smapi.targets | 2 - src/SMAPI.ModBuildConfig/package.nuspec | 8 +-- 6 files changed, 6 insertions(+), 132 deletions(-) delete mode 100644 src/SMAPI.ModBuildConfig.Analyzer/tools/install.ps1 delete mode 100644 src/SMAPI.ModBuildConfig.Analyzer/tools/uninstall.ps1 (limited to 'src') diff --git a/build/prepare-nuget-package.targets b/build/prepare-nuget-package.targets index 172bfdcc..8894c273 100644 --- a/build/prepare-nuget-package.targets +++ b/build/prepare-nuget-package.targets @@ -17,7 +17,5 @@ - - diff --git a/docs/mod-build-config.md b/docs/mod-build-config.md index a97c3171..329f5faf 100644 --- a/docs/mod-build-config.md +++ b/docs/mod-build-config.md @@ -216,6 +216,9 @@ That error means the package couldn't find your game. You can specify the game p _[Game path](#game-path)_ above. ## Release notes +### Upcoming release +* Dropped support for very old versions of SMAPI and Visual Studio. + ### 2.2 * Added support for SMAPI 2.8+ (still compatible with earlier versions). * Added default game paths for 32-bit Windows. diff --git a/src/SMAPI.ModBuildConfig.Analyzer/tools/install.ps1 b/src/SMAPI.ModBuildConfig.Analyzer/tools/install.ps1 deleted file mode 100644 index ff051759..00000000 --- a/src/SMAPI.ModBuildConfig.Analyzer/tools/install.ps1 +++ /dev/null @@ -1,58 +0,0 @@ -param($installPath, $toolsPath, $package, $project) - -if($project.Object.SupportsPackageDependencyResolution) -{ - if($project.Object.SupportsPackageDependencyResolution()) - { - # Do not install analyzers via install.ps1, instead let the project system handle it. - return - } -} - -$analyzersPaths = Join-Path (Join-Path (Split-Path -Path $toolsPath -Parent) "analyzers") * -Resolve - -foreach($analyzersPath in $analyzersPaths) -{ - if (Test-Path $analyzersPath) - { - # Install the language agnostic analyzers. - foreach ($analyzerFilePath in Get-ChildItem -Path "$analyzersPath\*.dll" -Exclude *.resources.dll) - { - if($project.Object.AnalyzerReferences) - { - $project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName) - } - } - } -} - -# $project.Type gives the language name like (C# or VB.NET) -$languageFolder = "" -if($project.Type -eq "C#") -{ - $languageFolder = "cs" -} -if($project.Type -eq "VB.NET") -{ - $languageFolder = "vb" -} -if($languageFolder -eq "") -{ - return -} - -foreach($analyzersPath in $analyzersPaths) -{ - # Install language specific analyzers. - $languageAnalyzersPath = join-path $analyzersPath $languageFolder - if (Test-Path $languageAnalyzersPath) - { - foreach ($analyzerFilePath in Get-ChildItem -Path "$languageAnalyzersPath\*.dll" -Exclude *.resources.dll) - { - if($project.Object.AnalyzerReferences) - { - $project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName) - } - } - } -} diff --git a/src/SMAPI.ModBuildConfig.Analyzer/tools/uninstall.ps1 b/src/SMAPI.ModBuildConfig.Analyzer/tools/uninstall.ps1 deleted file mode 100644 index 4bed3337..00000000 --- a/src/SMAPI.ModBuildConfig.Analyzer/tools/uninstall.ps1 +++ /dev/null @@ -1,65 +0,0 @@ -param($installPath, $toolsPath, $package, $project) - -if($project.Object.SupportsPackageDependencyResolution) -{ - if($project.Object.SupportsPackageDependencyResolution()) - { - # Do not uninstall analyzers via uninstall.ps1, instead let the project system handle it. - return - } -} - -$analyzersPaths = Join-Path (Join-Path (Split-Path -Path $toolsPath -Parent) "analyzers") * -Resolve - -foreach($analyzersPath in $analyzersPaths) -{ - # Uninstall the language agnostic analyzers. - if (Test-Path $analyzersPath) - { - foreach ($analyzerFilePath in Get-ChildItem -Path "$analyzersPath\*.dll" -Exclude *.resources.dll) - { - if($project.Object.AnalyzerReferences) - { - $project.Object.AnalyzerReferences.Remove($analyzerFilePath.FullName) - } - } - } -} - -# $project.Type gives the language name like (C# or VB.NET) -$languageFolder = "" -if($project.Type -eq "C#") -{ - $languageFolder = "cs" -} -if($project.Type -eq "VB.NET") -{ - $languageFolder = "vb" -} -if($languageFolder -eq "") -{ - return -} - -foreach($analyzersPath in $analyzersPaths) -{ - # Uninstall language specific analyzers. - $languageAnalyzersPath = join-path $analyzersPath $languageFolder - if (Test-Path $languageAnalyzersPath) - { - foreach ($analyzerFilePath in Get-ChildItem -Path "$languageAnalyzersPath\*.dll" -Exclude *.resources.dll) - { - if($project.Object.AnalyzerReferences) - { - try - { - $project.Object.AnalyzerReferences.Remove($analyzerFilePath.FullName) - } - catch - { - - } - } - } - } -} diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets index e6c3fa57..99011629 100644 --- a/src/SMAPI.ModBuildConfig/build/smapi.targets +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -102,7 +102,6 @@ $(GamePath)\smapi-internal\StardewModdingAPI.Toolkit.CoreInterfaces.dll - $(GamePath)\StardewModdingAPI.Toolkit.CoreInterfaces.dll false true @@ -142,7 +141,6 @@ $(GamePath)\smapi-internal\StardewModdingAPI.Toolkit.CoreInterfaces.dll - $(GamePath)\StardewModdingAPI.Toolkit.CoreInterfaces.dll false true diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec index 21693828..1d73c4a1 100644 --- a/src/SMAPI.ModBuildConfig/package.nuspec +++ b/src/SMAPI.ModBuildConfig/package.nuspec @@ -2,7 +2,7 @@ Pathoschild.Stardew.ModBuildConfig - 2.2 + 2.2.1 Build package for SMAPI mods Pathoschild Pathoschild @@ -12,10 +12,8 @@ https://raw.githubusercontent.com/Pathoschild/SMAPI/develop/src/SMAPI.ModBuildConfig/assets/nuget-icon.png Automates the build configuration for crossplatform Stardew Valley SMAPI mods. For Stardew Valley 1.3 or later. - 2.2: - - Added support for SMAPI 2.8+ (still compatible with earlier versions). - - Added default game paths for 32-bit Windows. - - Fixed valid manifests marked invalid in some cases. + 2.2.1: + - Dropped support for very old versions of SMAPI and Visual Studio. -- cgit From 6b347b83a7c588638224a02840b1d90b4557ea2a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 9 Feb 2019 15:16:14 -0500 Subject: fix Save Backup not pruning old backups if they're uncompressed --- docs/release-notes.md | 3 +++ src/SMAPI.Mods.SaveBackup/ModEntry.cs | 13 ++++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index efbd7ddb..7724904b 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,9 @@ ## 3.0 (upcoming release) These changes have not been released yet. +* For players: + * Fixed Save Backup not pruning old backups if they're uncompressed. + * For modders: * Added `IContentPack.HasFile` method. * Updated to Json.NET 12.0.1. diff --git a/src/SMAPI.Mods.SaveBackup/ModEntry.cs b/src/SMAPI.Mods.SaveBackup/ModEntry.cs index 30dbfbe6..33adf76d 100644 --- a/src/SMAPI.Mods.SaveBackup/ModEntry.cs +++ b/src/SMAPI.Mods.SaveBackup/ModEntry.cs @@ -124,20 +124,23 @@ namespace StardewModdingAPI.Mods.SaveBackup try { var oldBackups = backupFolder - .GetFiles() + .GetFileSystemInfos() .OrderByDescending(p => p.CreationTimeUtc) .Skip(backupsToKeep); - foreach (FileInfo file in oldBackups) + foreach (FileSystemInfo entry in oldBackups) { try { - this.Monitor.Log($"Deleting {file.Name}...", LogLevel.Trace); - file.Delete(); + this.Monitor.Log($"Deleting {entry.Name}...", LogLevel.Trace); + if (entry is DirectoryInfo folder) + folder.Delete(recursive: true); + else + entry.Delete(); } catch (Exception ex) { - this.Monitor.Log($"Error deleting old save backup '{file.Name}': {ex}", LogLevel.Error); + this.Monitor.Log($"Error deleting old save backup '{entry.Name}': {ex}", LogLevel.Error); } } } -- cgit From 0ba8325708d6eea6bb60121d85d4cad8ab4e7787 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 9 Feb 2019 19:16:54 -0500 Subject: fix Newtonsoft.Json.pdb not ignored for release zips --- src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs | 1 + src/SMAPI.ModBuildConfig/package.nuspec | 1 + 2 files changed, 2 insertions(+) (limited to 'src') diff --git a/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs b/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs index e03683d0..4af3100f 100644 --- a/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs +++ b/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs @@ -129,6 +129,7 @@ namespace StardewModdingAPI.ModBuildConfig.Framework // Json.NET (bundled into SMAPI) || this.EqualsInvariant(file.Name, "Newtonsoft.Json.dll") + || this.EqualsInvariant(file.Name, "Newtonsoft.Json.pdb") || this.EqualsInvariant(file.Name, "Newtonsoft.Json.xml") // code analysis files diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec index 1d73c4a1..d054fd1a 100644 --- a/src/SMAPI.ModBuildConfig/package.nuspec +++ b/src/SMAPI.ModBuildConfig/package.nuspec @@ -14,6 +14,7 @@ 2.2.1: - Dropped support for very old versions of SMAPI and Visual Studio. + - Fixed `Newtonsoft.Json.pdb` included in release zips when Json.NET is referenced directly. -- cgit From 4a297e29eb0c97be799ee3939e9747dc1c631427 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 18 Feb 2019 01:33:10 -0500 Subject: better handle player reconnecting before disconnection is registered --- docs/release-notes.md | 1 + src/SMAPI/Framework/SMultiplayer.cs | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 7724904b..3041dbd3 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ These changes have not been released yet. * For players: * Fixed Save Backup not pruning old backups if they're uncompressed. + * Fixed issues when a farmhand reconnects before the game notices they're disconnected. * For modders: * Added `IContentPack.HasFile` method. diff --git a/src/SMAPI/Framework/SMultiplayer.cs b/src/SMAPI/Framework/SMultiplayer.cs index 0241ef02..dde71092 100644 --- a/src/SMAPI/Framework/SMultiplayer.cs +++ b/src/SMAPI/Framework/SMultiplayer.cs @@ -201,7 +201,8 @@ namespace StardewModdingAPI.Framework MultiplayerPeer newPeer = new MultiplayerPeer(message.FarmerID, model, sendMessage, isHost: false); if (this.Peers.ContainsKey(message.FarmerID)) { - this.Monitor.Log($"Rejected mod context from farmhand {message.FarmerID}: already received context for that player.", LogLevel.Error); + this.Monitor.Log($"Received mod context from farmhand {message.FarmerID}, but the game didn't see them disconnect. This may indicate issues with the network connection.", LogLevel.Info); + this.Peers.Remove(message.FarmerID); return; } this.AddPeer(newPeer, canBeHost: false, raiseEvent: false); -- cgit From bad2ac2a29b8775be97133e4c4cfb67a4a7406ee Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 Feb 2019 19:56:08 -0500 Subject: remove deprecated APIs (#606) --- docs/release-notes.md | 1 + docs/technical-docs.md | 1 - .../ISemanticVersion.cs | 6 - src/SMAPI.Toolkit/SemanticVersion.cs | 19 +- .../Converters/SemanticVersionConverter.cs | 14 +- src/SMAPI/Constants.cs | 26 +-- src/SMAPI/Events/ContentEvents.cs | 45 ---- src/SMAPI/Events/ControlEvents.cs | 123 ---------- src/SMAPI/Events/EventArgsClickableMenuChanged.cs | 33 --- src/SMAPI/Events/EventArgsClickableMenuClosed.cs | 28 --- .../Events/EventArgsControllerButtonPressed.cs | 34 --- .../Events/EventArgsControllerButtonReleased.cs | 34 --- .../Events/EventArgsControllerTriggerPressed.cs | 39 ---- .../Events/EventArgsControllerTriggerReleased.cs | 39 ---- src/SMAPI/Events/EventArgsInput.cs | 64 ----- src/SMAPI/Events/EventArgsIntChanged.cs | 32 --- src/SMAPI/Events/EventArgsInventoryChanged.cs | 43 ---- src/SMAPI/Events/EventArgsKeyPressed.cs | 28 --- src/SMAPI/Events/EventArgsKeyboardStateChanged.cs | 33 --- src/SMAPI/Events/EventArgsLevelUp.cs | 55 ----- .../Events/EventArgsLocationBuildingsChanged.cs | 41 ---- .../Events/EventArgsLocationObjectsChanged.cs | 42 ---- src/SMAPI/Events/EventArgsLocationsChanged.cs | 35 --- src/SMAPI/Events/EventArgsMineLevelChanged.cs | 32 --- src/SMAPI/Events/EventArgsMouseStateChanged.cs | 44 ---- src/SMAPI/Events/EventArgsPlayerWarped.cs | 34 --- src/SMAPI/Events/EventArgsValueChanged.cs | 33 --- src/SMAPI/Events/GameEvents.cs | 122 ---------- src/SMAPI/Events/GraphicsEvents.cs | 120 ---------- src/SMAPI/Events/InputEvents.cs | 56 ----- src/SMAPI/Events/LocationEvents.cs | 67 ------ src/SMAPI/Events/MenuEvents.cs | 56 ----- src/SMAPI/Events/MineEvents.cs | 45 ---- src/SMAPI/Events/MultiplayerEvents.cs | 78 ------- src/SMAPI/Events/PlayerEvents.cs | 68 ------ src/SMAPI/Events/SaveEvents.cs | 100 -------- src/SMAPI/Events/SpecialisedEvents.cs | 45 ---- src/SMAPI/Events/TimeEvents.cs | 56 ----- .../Framework/Content/AssetDataForDictionary.cs | 33 --- src/SMAPI/Framework/DeprecationManager.cs | 29 --- src/SMAPI/Framework/Events/EventManager.cs | 260 --------------------- src/SMAPI/Framework/Events/ManagedEvent.cs | 105 +++++---- src/SMAPI/Framework/Events/ManagedEventBase.cs | 93 -------- src/SMAPI/Framework/ModHelpers/ModHelper.cs | 71 +----- src/SMAPI/Framework/ModLoading/ModResolver.cs | 4 - src/SMAPI/Framework/SCore.cs | 40 +--- src/SMAPI/Framework/SGame.cs | 207 +--------------- src/SMAPI/Framework/SMultiplayer.cs | 18 -- src/SMAPI/IAssetDataForDictionary.cs | 27 +-- src/SMAPI/IModHelper.cs | 38 --- src/SMAPI/Metadata/InstructionMetadata.cs | 3 - src/SMAPI/SemanticVersion.cs | 14 -- 52 files changed, 65 insertions(+), 2648 deletions(-) delete mode 100644 src/SMAPI/Events/ContentEvents.cs delete mode 100644 src/SMAPI/Events/ControlEvents.cs delete mode 100644 src/SMAPI/Events/EventArgsClickableMenuChanged.cs delete mode 100644 src/SMAPI/Events/EventArgsClickableMenuClosed.cs delete mode 100644 src/SMAPI/Events/EventArgsControllerButtonPressed.cs delete mode 100644 src/SMAPI/Events/EventArgsControllerButtonReleased.cs delete mode 100644 src/SMAPI/Events/EventArgsControllerTriggerPressed.cs delete mode 100644 src/SMAPI/Events/EventArgsControllerTriggerReleased.cs delete mode 100644 src/SMAPI/Events/EventArgsInput.cs delete mode 100644 src/SMAPI/Events/EventArgsIntChanged.cs delete mode 100644 src/SMAPI/Events/EventArgsInventoryChanged.cs delete mode 100644 src/SMAPI/Events/EventArgsKeyPressed.cs delete mode 100644 src/SMAPI/Events/EventArgsKeyboardStateChanged.cs delete mode 100644 src/SMAPI/Events/EventArgsLevelUp.cs delete mode 100644 src/SMAPI/Events/EventArgsLocationBuildingsChanged.cs delete mode 100644 src/SMAPI/Events/EventArgsLocationObjectsChanged.cs delete mode 100644 src/SMAPI/Events/EventArgsLocationsChanged.cs delete mode 100644 src/SMAPI/Events/EventArgsMineLevelChanged.cs delete mode 100644 src/SMAPI/Events/EventArgsMouseStateChanged.cs delete mode 100644 src/SMAPI/Events/EventArgsPlayerWarped.cs delete mode 100644 src/SMAPI/Events/EventArgsValueChanged.cs delete mode 100644 src/SMAPI/Events/GameEvents.cs delete mode 100644 src/SMAPI/Events/GraphicsEvents.cs delete mode 100644 src/SMAPI/Events/InputEvents.cs delete mode 100644 src/SMAPI/Events/LocationEvents.cs delete mode 100644 src/SMAPI/Events/MenuEvents.cs delete mode 100644 src/SMAPI/Events/MineEvents.cs delete mode 100644 src/SMAPI/Events/MultiplayerEvents.cs delete mode 100644 src/SMAPI/Events/PlayerEvents.cs delete mode 100644 src/SMAPI/Events/SaveEvents.cs delete mode 100644 src/SMAPI/Events/SpecialisedEvents.cs delete mode 100644 src/SMAPI/Events/TimeEvents.cs delete mode 100644 src/SMAPI/Framework/Events/ManagedEventBase.cs (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 3041dbd3..2f01ee93 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ These changes have not been released yet. * For modders: * Added `IContentPack.HasFile` method. + * Dropped support for all deprecated APIs. * Updated to Json.NET 12.0.1. ## 2.11.3 diff --git a/docs/technical-docs.md b/docs/technical-docs.md index 98dd3540..545f4aa3 100644 --- a/docs/technical-docs.md +++ b/docs/technical-docs.md @@ -106,7 +106,6 @@ SMAPI uses a small number of conditional compilation constants, which you can se flag | purpose ---- | ------- `SMAPI_FOR_WINDOWS` | Whether SMAPI is being compiled on Windows for players on Windows. Set automatically in `crossplatform.targets`. -`SMAPI_3_0_STRICT` | Whether to exclude all deprecated APIs from compilation. This is useful for testing mods for SMAPI 3.0 compatibility. # SMAPI web services ## Overview diff --git a/src/SMAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs b/src/SMAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs index 0a6e5758..4a61f9ae 100644 --- a/src/SMAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs +++ b/src/SMAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs @@ -17,12 +17,6 @@ namespace StardewModdingAPI /// The patch version for backwards-compatible bug fixes. int PatchVersion { get; } -#if !SMAPI_3_0_STRICT - /// An optional build tag. - [Obsolete("Use " + nameof(ISemanticVersion.PrereleaseTag) + " instead")] - string Build { get; } -#endif - /// An optional prerelease tag. string PrereleaseTag { get; } diff --git a/src/SMAPI.Toolkit/SemanticVersion.cs b/src/SMAPI.Toolkit/SemanticVersion.cs index ba9ca6c6..78b4c007 100644 --- a/src/SMAPI.Toolkit/SemanticVersion.cs +++ b/src/SMAPI.Toolkit/SemanticVersion.cs @@ -42,15 +42,6 @@ namespace StardewModdingAPI.Toolkit /// An optional prerelease tag. public string PrereleaseTag { get; } -#if !SMAPI_3_0_STRICT - /// An optional prerelease tag. - [Obsolete("Use " + nameof(ISemanticVersion.PrereleaseTag) + " instead")] - public string Build => this.PrereleaseTag; - - /// Whether the version was parsed from the legacy object format. - public bool IsLegacyFormat { get; } -#endif - /********* ** Public methods @@ -60,20 +51,12 @@ namespace StardewModdingAPI.Toolkit /// The minor version incremented for backwards-compatible changes. /// The patch version for backwards-compatible fixes. /// An optional prerelease tag. - /// Whether the version was parsed from the legacy object format. - public SemanticVersion(int major, int minor, int patch, string prereleaseTag = null -#if !SMAPI_3_0_STRICT - , bool isLegacyFormat = false -#endif - ) + public SemanticVersion(int major, int minor, int patch, string prereleaseTag = null) { this.MajorVersion = major; this.MinorVersion = minor; this.PatchVersion = patch; this.PrereleaseTag = this.GetNormalisedTag(prereleaseTag); -#if !SMAPI_3_0_STRICT - this.IsLegacyFormat = isLegacyFormat; -#endif this.AssertValid(); } diff --git a/src/SMAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs b/src/SMAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs index aca06849..c50de4db 100644 --- a/src/SMAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs +++ b/src/SMAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs @@ -67,20 +67,8 @@ namespace StardewModdingAPI.Toolkit.Serialisation.Converters int minor = obj.ValueIgnoreCase(nameof(ISemanticVersion.MinorVersion)); int patch = obj.ValueIgnoreCase(nameof(ISemanticVersion.PatchVersion)); string prereleaseTag = obj.ValueIgnoreCase(nameof(ISemanticVersion.PrereleaseTag)); -#if !SMAPI_3_0_STRICT - if (string.IsNullOrWhiteSpace(prereleaseTag)) - { - prereleaseTag = obj.ValueIgnoreCase("Build"); - if (prereleaseTag == "0") - prereleaseTag = null; // '0' from incorrect examples in old SMAPI documentation - } -#endif - return new SemanticVersion(major, minor, patch, prereleaseTag -#if !SMAPI_3_0_STRICT - , isLegacyFormat: true -#endif - ); + return new SemanticVersion(major, minor, patch, prereleaseTag); } /// Read a JSON string. diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 9d686e2f..736a10ea 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -44,32 +44,10 @@ namespace StardewModdingAPI public static string SavesPath { get; } = Path.Combine(Constants.DataPath, "Saves"); /// The name of the current save folder (if save info is available, regardless of whether the save file exists yet). - public static string SaveFolderName - { - get - { - return Constants.GetSaveFolderName() -#if SMAPI_3_0_STRICT - ; -#else - ?? ""; -#endif - } - } + public static string SaveFolderName => Constants.GetSaveFolderName(); /// The absolute path to the current save folder (if save info is available and the save file exists). - public static string CurrentSavePath - { - get - { - return Constants.GetSaveFolderPathIfExists() -#if SMAPI_3_0_STRICT - ; -#else - ?? ""; -#endif - } - } + public static string CurrentSavePath => Constants.GetSaveFolderPathIfExists(); /**** ** Internal diff --git a/src/SMAPI/Events/ContentEvents.cs b/src/SMAPI/Events/ContentEvents.cs deleted file mode 100644 index aca76ef7..00000000 --- a/src/SMAPI/Events/ContentEvents.cs +++ /dev/null @@ -1,45 +0,0 @@ -#if !SMAPI_3_0_STRICT -using System; -using StardewModdingAPI.Framework; -using StardewModdingAPI.Framework.Events; - -namespace StardewModdingAPI.Events -{ - /// Events raised when the game loads content. - [Obsolete("Use " + nameof(Mod.Helper) + "." + nameof(IModHelper.Events) + " instead. See https://smapi.io/3.0 for more info.")] - public static class ContentEvents - { - /********* - ** Fields - *********/ - /// The core event manager. - private static EventManager EventManager; - - - /********* - ** Events - *********/ - /// Raised after the content language changes. - public static event EventHandler> AfterLocaleChanged - { - add - { - SCore.DeprecationManager.WarnForOldEvents(); - ContentEvents.EventManager.Legacy_LocaleChanged.Add(value); - } - remove => ContentEvents.EventManager.Legacy_LocaleChanged.Remove(value); - } - - - /********* - ** Public methods - *********/ - /// Initialise the events. - /// The core event manager. - internal static void Init(EventManager eventManager) - { - ContentEvents.EventManager = eventManager; - } - } -} -#endif diff --git a/src/SMAPI/Events/ControlEvents.cs b/src/SMAPI/Events/ControlEvents.cs deleted file mode 100644 index 45aedc9b..00000000 --- a/src/SMAPI/Events/ControlEvents.cs +++ /dev/null @@ -1,123 +0,0 @@ -#if !SMAPI_3_0_STRICT -using System; -using Microsoft.Xna.Framework.Input; -using StardewModdingAPI.Framework; -using StardewModdingAPI.Framework.Events; - -namespace StardewModdingAPI.Events -{ - /// Events raised when the player uses a controller, keyboard, or mouse. - [Obsolete("Use " + nameof(Mod.Helper) + "." + nameof(IModHelper.Events) + " instead. See https://smapi.io/3.0 for more info.")] - public static class ControlEvents - { - /********* - ** Fields - *********/ - /// The core event manager. - private static EventManager EventManager; - - - /********* - ** Events - *********/ - /// Raised when the changes. That happens when the player presses or releases a key. - public static event EventHandler KeyboardChanged - { - add - { - SCore.DeprecationManager.WarnForOldEvents(); - ControlEvents.EventManager.Legacy_KeyboardChanged.Add(value); - } - remove => ControlEvents.EventManager.Legacy_KeyboardChanged.Remove(value); - } - - /// Raised after the player presses a keyboard key. - public static event EventHandler KeyPressed - { - add - { - SCore.DeprecationManager.WarnForOldEvents(); - ControlEvents.EventManager.Legacy_KeyPressed.Add(value); - } - remove => ControlEvents.EventManager.Legacy_KeyPressed.Remove(value); - } - - /// Raised after the player releases a keyboard key. - public static event EventHandler KeyReleased - { - add - { - SCore.DeprecationManager.WarnForOldEvents(); - ControlEvents.EventManager.Legacy_KeyReleased.Add(value); - } - remove => ControlEvents.EventManager.Legacy_KeyReleased.Remove(value); - } - - /// Raised when the changes. That happens when the player moves the mouse, scrolls the mouse wheel, or presses/releases a button. - public static event EventHandler MouseChanged - { - add - { - SCore.DeprecationManager.WarnForOldEvents(); - ControlEvents.EventManager.Legacy_MouseChanged.Add(value); - } - remove => ControlEvents.EventManager.Legacy_MouseChanged.Remove(value); - } - - /// The player pressed a controller button. This event isn't raised for trigger buttons. - public static event EventHandler ControllerButtonPressed - { - add - { - SCore.DeprecationManager.WarnForOldEvents(); - ControlEvents.EventManager.Legacy_ControllerButtonPressed.Add(value); - } - remove => ControlEvents.EventManager.Legacy_ControllerButtonPressed.Remove(value); - } - - /// The player released a controller button. This event isn't raised for trigger buttons. - public static event EventHandler ControllerButtonReleased - { - add - { - SCore.DeprecationManager.WarnForOldEvents(); - ControlEvents.EventManager.Legacy_ControllerButtonReleased.Add(value); - } - remove => ControlEvents.EventManager.Legacy_ControllerButtonReleased.Remove(value); - } - - /// The player pressed a controller trigger button. - public static event EventHandler ControllerTriggerPressed - { - add - { - SCore.DeprecationManager.WarnForOldEvents(); - ControlEvents.EventManager.Legacy_ControllerTriggerPressed.Add(value); - } - remove => ControlEvents.EventManager.Legacy_ControllerTriggerPressed.Remove(value); - } - - /// The player released a controller trigger button. - public static event EventHandler ControllerTriggerReleased - { - add - { - SCore.DeprecationManager.WarnForOldEvents(); - ControlEvents.EventManager.Legacy_ControllerTriggerReleased.Add(value); - } - remove => ControlEvents.EventManager.Legacy_ControllerTriggerReleased.Remove(value); - } - - - /********* - ** Public methods - *********/ - /// Initialise the events. - /// The core event manager. - internal static void Init(EventManager eventManager) - { - ControlEvents.EventManager = eventManager; - } - } -} -#endif diff --git a/src/SMAPI/Events/EventArgsClickableMenuChanged.cs b/src/SMAPI/Events/EventArgsClickableMenuChanged.cs deleted file mode 100644 index a0b903b7..00000000 --- a/src/SMAPI/Events/EventArgsClickableMenuChanged.cs +++ /dev/null @@ -1,33 +0,0 @@ -#if !SMAPI_3_0_STRICT -using System; -using StardewValley.Menus; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class EventArgsClickableMenuChanged : EventArgs - { - /********* - ** Accessors - *********/ - /// The previous menu. - public IClickableMenu NewMenu { get; } - - /// The current menu. - public IClickableMenu PriorMenu { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The previous menu. - /// The current menu. - public EventArgsClickableMenuChanged(IClickableMenu priorMenu, IClickableMenu newMenu) - { - this.NewMenu = newMenu; - this.PriorMenu = priorMenu; - } - } -} -#endif diff --git a/src/SMAPI/Events/EventArgsClickableMenuClosed.cs b/src/SMAPI/Events/EventArgsClickableMenuClosed.cs deleted file mode 100644 index 77db69ea..00000000 --- a/src/SMAPI/Events/EventArgsClickableMenuClosed.cs +++ /dev/null @@ -1,28 +0,0 @@ -#if !SMAPI_3_0_STRICT -using System; -using StardewValley.Menus; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class EventArgsClickableMenuClosed : EventArgs - { - /********* - ** Accessors - *********/ - /// The menu that was closed. - public IClickableMenu PriorMenu { get; } - - - /********* - ** Accessors - *********/ - /// Construct an instance. - /// The menu that was closed. - public EventArgsClickableMenuClosed(IClickableMenu priorMenu) - { - this.PriorMenu = priorMenu; - } - } -} -#endif diff --git a/src/SMAPI/Events/EventArgsControllerButtonPressed.cs b/src/SMAPI/Events/EventArgsControllerButtonPressed.cs deleted file mode 100644 index 949446e1..00000000 --- a/src/SMAPI/Events/EventArgsControllerButtonPressed.cs +++ /dev/null @@ -1,34 +0,0 @@ -#if !SMAPI_3_0_STRICT -using System; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Input; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class EventArgsControllerButtonPressed : EventArgs - { - /********* - ** Accessors - *********/ - /// The player who pressed the button. - public PlayerIndex PlayerIndex { get; } - - /// The controller button that was pressed. - public Buttons ButtonPressed { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The player who pressed the button. - /// The controller button that was pressed. - public EventArgsControllerButtonPressed(PlayerIndex playerIndex, Buttons button) - { - this.PlayerIndex = playerIndex; - this.ButtonPressed = button; - } - } -} -#endif diff --git a/src/SMAPI/Events/EventArgsControllerButtonReleased.cs b/src/SMAPI/Events/EventArgsControllerButtonReleased.cs deleted file mode 100644 index d6d6d840..00000000 --- a/src/SMAPI/Events/EventArgsControllerButtonReleased.cs +++ /dev/null @@ -1,34 +0,0 @@ -#if !SMAPI_3_0_STRICT -using System; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Input; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class EventArgsControllerButtonReleased : EventArgs - { - /********* - ** Accessors - *********/ - /// The player who pressed the button. - public PlayerIndex PlayerIndex { get; } - - /// The controller button that was pressed. - public Buttons ButtonReleased { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The player who pressed the button. - /// The controller button that was released. - public EventArgsControllerButtonReleased(PlayerIndex playerIndex, Buttons button) - { - this.PlayerIndex = playerIndex; - this.ButtonReleased = button; - } - } -} -#endif diff --git a/src/SMAPI/Events/EventArgsControllerTriggerPressed.cs b/src/SMAPI/Events/EventArgsControllerTriggerPressed.cs deleted file mode 100644 index 33be2fa3..00000000 --- a/src/SMAPI/Events/EventArgsControllerTriggerPressed.cs +++ /dev/null @@ -1,39 +0,0 @@ -#if !SMAPI_3_0_STRICT -using System; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Input; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class EventArgsControllerTriggerPressed : EventArgs - { - /********* - ** Accessors - *********/ - /// The player who pressed the button. - public PlayerIndex PlayerIndex { get; } - - /// The controller button that was pressed. - public Buttons ButtonPressed { get; } - - /// The current trigger value. - public float Value { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The player who pressed the trigger button. - /// The trigger button that was pressed. - /// The current trigger value. - public EventArgsControllerTriggerPressed(PlayerIndex playerIndex, Buttons button, float value) - { - this.PlayerIndex = playerIndex; - this.ButtonPressed = button; - this.Value = value; - } - } -} -#endif diff --git a/src/SMAPI/Events/EventArgsControllerTriggerReleased.cs b/src/SMAPI/Events/EventArgsControllerTriggerReleased.cs deleted file mode 100644 index e90ff712..00000000 --- a/src/SMAPI/Events/EventArgsControllerTriggerReleased.cs +++ /dev/null @@ -1,39 +0,0 @@ -#if !SMAPI_3_0_STRICT -using System; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Input; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class EventArgsControllerTriggerReleased : EventArgs - { - /********* - ** Accessors - *********/ - /// The player who pressed the button. - public PlayerIndex PlayerIndex { get; } - - /// The controller button that was released. - public Buttons ButtonReleased { get; } - - /// The current trigger value. - public float Value { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The player who pressed the trigger button. - /// The trigger button that was released. - /// The current trigger value. - public EventArgsControllerTriggerReleased(PlayerIndex playerIndex, Buttons button, float value) - { - this.PlayerIndex = playerIndex; - this.ButtonReleased = button; - this.Value = value; - } - } -} -#endif diff --git a/src/SMAPI/Events/EventArgsInput.cs b/src/SMAPI/Events/EventArgsInput.cs deleted file mode 100644 index 5cff3408..00000000 --- a/src/SMAPI/Events/EventArgsInput.cs +++ /dev/null @@ -1,64 +0,0 @@ -#if !SMAPI_3_0_STRICT -using System; -using System.Collections.Generic; - -namespace StardewModdingAPI.Events -{ - /// Event arguments when a button is pressed or released. - public class EventArgsInput : EventArgs - { - /********* - ** Fields - *********/ - /// The buttons to suppress. - private readonly HashSet SuppressButtons; - - - /********* - ** Accessors - *********/ - /// The button on the controller, keyboard, or mouse. - public SButton Button { get; } - - /// The current cursor position. - public ICursorPosition Cursor { get; } - - /// Whether the input should trigger actions on the affected tile. - public bool IsActionButton => this.Button.IsActionButton(); - - /// Whether the input should use tools on the affected tile. - public bool IsUseToolButton => this.Button.IsUseToolButton(); - - /// Whether a mod has indicated the key was already handled. - public bool IsSuppressed => this.SuppressButtons.Contains(this.Button); - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The button on the controller, keyboard, or mouse. - /// The cursor position. - /// The buttons to suppress. - public EventArgsInput(SButton button, ICursorPosition cursor, HashSet suppressButtons) - { - this.Button = button; - this.Cursor = cursor; - this.SuppressButtons = suppressButtons; - } - - /// Prevent the game from handling the current button press. This doesn't prevent other mods from receiving the event. - public void SuppressButton() - { - this.SuppressButton(this.Button); - } - - /// Prevent the game from handling a button press. This doesn't prevent other mods from receiving the event. - /// The button to suppress. - public void SuppressButton(SButton button) - { - this.SuppressButtons.Add(button); - } - } -} -#endif diff --git a/src/SMAPI/Events/EventArgsIntChanged.cs b/src/SMAPI/Events/EventArgsIntChanged.cs deleted file mode 100644 index 76ec6d08..00000000 --- a/src/SMAPI/Events/EventArgsIntChanged.cs +++ /dev/null @@ -1,32 +0,0 @@ -#if !SMAPI_3_0_STRICT -using System; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for an integer field that changed value. - public class EventArgsIntChanged : EventArgs - { - /********* - ** Accessors - *********/ - /// The previous value. - public int PriorInt { get; } - - /// The current value. - public int NewInt { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The previous value. - /// The current value. - public EventArgsIntChanged(int priorInt, int newInt) - { - this.PriorInt = priorInt; - this.NewInt = newInt; - } - } -} -#endif diff --git a/src/SMAPI/Events/EventArgsInventoryChanged.cs b/src/SMAPI/Events/EventArgsInventoryChanged.cs deleted file mode 100644 index 488dd23f..00000000 --- a/src/SMAPI/Events/EventArgsInventoryChanged.cs +++ /dev/null @@ -1,43 +0,0 @@ -#if !SMAPI_3_0_STRICT -using System; -using System.Collections.Generic; -using System.Linq; -using StardewValley; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class EventArgsInventoryChanged : EventArgs - { - /********* - ** Accessors - *********/ - /// The player's inventory. - public IList Inventory { get; } - - /// The added items. - public List Added { get; } - - /// The removed items. - public List Removed { get; } - - /// The items whose stack sizes changed. - public List QuantityChanged { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The player's inventory. - /// The inventory changes. - public EventArgsInventoryChanged(IList inventory, ItemStackChange[] changedItems) - { - this.Inventory = inventory; - this.Added = changedItems.Where(n => n.ChangeType == ChangeType.Added).ToList(); - this.Removed = changedItems.Where(n => n.ChangeType == ChangeType.Removed).ToList(); - this.QuantityChanged = changedItems.Where(n => n.ChangeType == ChangeType.StackChange).ToList(); - } - } -} -#endif diff --git a/src/SMAPI/Events/EventArgsKeyPressed.cs b/src/SMAPI/Events/EventArgsKeyPressed.cs deleted file mode 100644 index 6204d821..00000000 --- a/src/SMAPI/Events/EventArgsKeyPressed.cs +++ /dev/null @@ -1,28 +0,0 @@ -#if !SMAPI_3_0_STRICT -using System; -using Microsoft.Xna.Framework.Input; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class EventArgsKeyPressed : EventArgs - { - /********* - ** Accessors - *********/ - /// The keyboard button that was pressed. - public Keys KeyPressed { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The keyboard button that was pressed. - public EventArgsKeyPressed(Keys key) - { - this.KeyPressed = key; - } - } -} -#endif diff --git a/src/SMAPI/Events/EventArgsKeyboardStateChanged.cs b/src/SMAPI/Events/EventArgsKeyboardStateChanged.cs deleted file mode 100644 index 2c3203b1..00000000 --- a/src/SMAPI/Events/EventArgsKeyboardStateChanged.cs +++ /dev/null @@ -1,33 +0,0 @@ -#if !SMAPI_3_0_STRICT -using System; -using Microsoft.Xna.Framework.Input; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class EventArgsKeyboardStateChanged : EventArgs - { - /********* - ** Accessors - *********/ - /// The previous keyboard state. - public KeyboardState NewState { get; } - - /// The current keyboard state. - public KeyboardState PriorState { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The previous keyboard state. - /// The current keyboard state. - public EventArgsKeyboardStateChanged(KeyboardState priorState, KeyboardState newState) - { - this.PriorState = priorState; - this.NewState = newState; - } - } -} -#endif diff --git a/src/SMAPI/Events/EventArgsLevelUp.cs b/src/SMAPI/Events/EventArgsLevelUp.cs deleted file mode 100644 index 06c70088..00000000 --- a/src/SMAPI/Events/EventArgsLevelUp.cs +++ /dev/null @@ -1,55 +0,0 @@ -#if !SMAPI_3_0_STRICT -using System; -using StardewModdingAPI.Enums; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class EventArgsLevelUp : EventArgs - { - /********* - ** Accessors - *********/ - /// The player skill that leveled up. - public LevelType Type { get; } - - /// The new skill level. - public int NewLevel { get; } - - /// The player skill types. - public enum LevelType - { - /// The combat skill. - Combat = SkillType.Combat, - - /// The farming skill. - Farming = SkillType.Farming, - - /// The fishing skill. - Fishing = SkillType.Fishing, - - /// The foraging skill. - Foraging = SkillType.Foraging, - - /// The mining skill. - Mining = SkillType.Mining, - - /// The luck skill. - Luck = SkillType.Luck - } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The player skill that leveled up. - /// The new skill level. - public EventArgsLevelUp(LevelType type, int newLevel) - { - this.Type = type; - this.NewLevel = newLevel; - } - } -} -#endif diff --git a/src/SMAPI/Events/EventArgsLocationBuildingsChanged.cs b/src/SMAPI/Events/EventArgsLocationBuildingsChanged.cs deleted file mode 100644 index 25e84722..00000000 --- a/src/SMAPI/Events/EventArgsLocationBuildingsChanged.cs +++ /dev/null @@ -1,41 +0,0 @@ -#if !SMAPI_3_0_STRICT -using System; -using System.Collections.Generic; -using System.Linq; -using StardewValley; -using StardewValley.Buildings; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class EventArgsLocationBuildingsChanged : EventArgs - { - /********* - ** Accessors - *********/ - /// The location which changed. - public GameLocation Location { get; } - - /// The buildings added to the location. - public IEnumerable Added { get; } - - /// The buildings removed from the location. - public IEnumerable Removed { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The location which changed. - /// The buildings added to the location. - /// The buildings removed from the location. - public EventArgsLocationBuildingsChanged(GameLocation location, IEnumerable added, IEnumerable removed) - { - this.Location = location; - this.Added = added.ToArray(); - this.Removed = removed.ToArray(); - } - } -} -#endif diff --git a/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs b/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs deleted file mode 100644 index 9ca2e3e2..00000000 --- a/src/SMAPI/Events/EventArgsLocationObjectsChanged.cs +++ /dev/null @@ -1,42 +0,0 @@ -#if !SMAPI_3_0_STRICT -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Xna.Framework; -using StardewValley; -using SObject = StardewValley.Object; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class EventArgsLocationObjectsChanged : EventArgs - { - /********* - ** Accessors - *********/ - /// The location which changed. - public GameLocation Location { get; } - - /// The objects added to the location. - public IEnumerable> Added { get; } - - /// The objects removed from the location. - public IEnumerable> Removed { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The location which changed. - /// The objects added to the location. - /// The objects removed from the location. - public EventArgsLocationObjectsChanged(GameLocation location, IEnumerable> added, IEnumerable> removed) - { - this.Location = location; - this.Added = added.ToArray(); - this.Removed = removed.ToArray(); - } - } -} -#endif diff --git a/src/SMAPI/Events/EventArgsLocationsChanged.cs b/src/SMAPI/Events/EventArgsLocationsChanged.cs deleted file mode 100644 index 1a59e612..00000000 --- a/src/SMAPI/Events/EventArgsLocationsChanged.cs +++ /dev/null @@ -1,35 +0,0 @@ -#if !SMAPI_3_0_STRICT -using System; -using System.Collections.Generic; -using System.Linq; -using StardewValley; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class EventArgsLocationsChanged : EventArgs - { - /********* - ** Accessors - *********/ - /// The added locations. - public IEnumerable Added { get; } - - /// The removed locations. - public IEnumerable Removed { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The added locations. - /// The removed locations. - public EventArgsLocationsChanged(IEnumerable added, IEnumerable removed) - { - this.Added = added.ToArray(); - this.Removed = removed.ToArray(); - } - } -} -#endif diff --git a/src/SMAPI/Events/EventArgsMineLevelChanged.cs b/src/SMAPI/Events/EventArgsMineLevelChanged.cs deleted file mode 100644 index c63b04e9..00000000 --- a/src/SMAPI/Events/EventArgsMineLevelChanged.cs +++ /dev/null @@ -1,32 +0,0 @@ -#if !SMAPI_3_0_STRICT -using System; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class EventArgsMineLevelChanged : EventArgs - { - /********* - ** Accessors - *********/ - /// The previous mine level. - public int PreviousMineLevel { get; } - - /// The current mine level. - public int CurrentMineLevel { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The previous mine level. - /// The current mine level. - public EventArgsMineLevelChanged(int previousMineLevel, int currentMineLevel) - { - this.PreviousMineLevel = previousMineLevel; - this.CurrentMineLevel = currentMineLevel; - } - } -} -#endif diff --git a/src/SMAPI/Events/EventArgsMouseStateChanged.cs b/src/SMAPI/Events/EventArgsMouseStateChanged.cs deleted file mode 100644 index 09f3f759..00000000 --- a/src/SMAPI/Events/EventArgsMouseStateChanged.cs +++ /dev/null @@ -1,44 +0,0 @@ -#if !SMAPI_3_0_STRICT -using System; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Input; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class EventArgsMouseStateChanged : EventArgs - { - /********* - ** Accessors - *********/ - /// The previous mouse state. - public MouseState PriorState { get; } - - /// The current mouse state. - public MouseState NewState { get; } - - /// The previous mouse position on the screen adjusted for the zoom level. - public Point PriorPosition { get; } - - /// The current mouse position on the screen adjusted for the zoom level. - public Point NewPosition { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The previous mouse state. - /// The current mouse state. - /// The previous mouse position on the screen adjusted for the zoom level. - /// The current mouse position on the screen adjusted for the zoom level. - public EventArgsMouseStateChanged(MouseState priorState, MouseState newState, Point priorPosition, Point newPosition) - { - this.PriorState = priorState; - this.NewState = newState; - this.PriorPosition = priorPosition; - this.NewPosition = newPosition; - } - } -} -#endif diff --git a/src/SMAPI/Events/EventArgsPlayerWarped.cs b/src/SMAPI/Events/EventArgsPlayerWarped.cs deleted file mode 100644 index d1aa1588..00000000 --- a/src/SMAPI/Events/EventArgsPlayerWarped.cs +++ /dev/null @@ -1,34 +0,0 @@ -#if !SMAPI_3_0_STRICT -using System; -using StardewValley; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a event. - public class EventArgsPlayerWarped : EventArgs - { - /********* - ** Accessors - *********/ - /// The player's previous location. - public GameLocation PriorLocation { get; } - - /// The player's current location. - public GameLocation NewLocation { get; } - - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The player's previous location. - /// The player's current location. - public EventArgsPlayerWarped(GameLocation priorLocation, GameLocation newLocation) - { - this.NewLocation = newLocation; - this.PriorLocation = priorLocation; - } - } -} -#endif diff --git a/src/SMAPI/Events/EventArgsValueChanged.cs b/src/SMAPI/Events/EventArgsValueChanged.cs deleted file mode 100644 index 7bfac7a2..00000000 --- a/src/SMAPI/Events/EventArgsValueChanged.cs +++ /dev/null @@ -1,33 +0,0 @@ -#if !SMAPI_3_0_STRICT -using System; - -namespace StardewModdingAPI.Events -{ - /// Event arguments for a field that changed value. - /// The value type. - public class EventArgsValueChanged : EventArgs - { - /********* - ** Accessors - *********/ - /// The previous value. - public T PriorValue { get; } - - /// The current value. - public T NewValue { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The previous value. - /// The current value. - public EventArgsValueChanged(T priorValue, T newValue) - { - this.PriorValue = priorValue; - this.NewValue = newValue; - } - } -} -#endif diff --git a/src/SMAPI/Events/GameEvents.cs b/src/SMAPI/Events/GameEvents.cs deleted file mode 100644 index 9d945277..00000000 --- a/src/SMAPI/Events/GameEvents.cs +++ /dev/null @@ -1,122 +0,0 @@ -#if !SMAPI_3_0_STRICT -using System; -using StardewModdingAPI.Framework; -using StardewModdingAPI.Framework.Events; - -namespace StardewModdingAPI.Events -{ - /// Events raised when the game changes state. - [Obsolete("Use " + nameof(Mod.Helper) + "." + nameof(IModHelper.Events) + " instead. See https://smapi.io/3.0 for more info.")] - public static class GameEvents - { - /********* - ** Fields - *********/ - /// The core event manager. - private static EventManager EventManager; - - - /********* - ** Events - *********/ - /// Raised when the game updates its state (≈60 times per second). - public static event EventHandler UpdateTick - { - add - { - SCore.DeprecationManager.WarnForOldEvents(); - GameEvents.EventManager.Legacy_UpdateTick.Add(value); - } - remove => GameEvents.EventManager.Legacy_UpdateTick.Remove(value); - } - - /// Raised every other tick (≈30 times per second). - public static event EventHandler SecondUpdateTick - { - add - { - SCore.DeprecationManager.WarnForOldEvents(); - GameEvents.EventManager.Legacy_SecondUpdateTick.Add(value); - } - remove => GameEvents.EventManager.Legacy_SecondUpdateTick.Remove(value); - } - - /// Raised every fourth tick (≈15 times per second). - public static event EventHandler FourthUpdateTick - { - add - { - SCore.DeprecationManager.WarnForOldEvents(); - GameEvents.EventManager.Legacy_FourthUpdateTick.Add(value); - } - remove => GameEvents.EventManager.Legacy_FourthUpdateTick.Remove(value); - } - - /// Raised every eighth tick (≈8 times per second). - public static event EventHandler EighthUpdateTick - { - add - { - SCore.DeprecationManager.WarnForOldEvents(); - GameEvents.EventManager.Legacy_EighthUpdateTick.Add(value); - } - remove => GameEvents.EventManager.Legacy_EighthUpdateTick.Remove(value); - } - - /// Raised every 15th tick (≈4 times per second). - public static event EventHandler QuarterSecondTick - { - add - { - SCore.DeprecationManager.WarnForOldEvents(); - GameEvents.EventManager.Legacy_QuarterSecondTick.Add(value); - } - remove => GameEvents.EventManager.Legacy_QuarterSecondTick.Remove(value); - } - - /// Raised every 30th tick (≈twice per second). - public static event EventHandler HalfSecondTick - { - add - { - SCore.DeprecationManager.WarnForOldEvents(); - GameEvents.EventManager.Legacy_HalfSecondTick.Add(value); - } - remove => GameEvents.EventManager.Legacy_HalfSecondTick.Remove(value); - } - - /// Raised every 60th tick (≈once per second). - public static event EventHandler OneSecondTick - { - add - { - SCore.DeprecationManager.WarnForOldEvents(); - GameEvents.EventManager.Legacy_OneSecondTick.Add(value); - } - remove => GameEvents.EventManager.Legacy_OneSecondTick.Remove(value); - } - - /// Raised once after the game initialises and all methods have been called. - public static event EventHandler FirstUpdateTick - { - add - { - SCore.DeprecationManager.WarnForOldEvents(); - GameEvents.EventManager.Legacy_FirstUpdateTick.Add(value); - } - remove => GameEvents.EventManager.Legacy_FirstUpdateTick.Remove(value); - } - - - /********* - ** Public methods - *********/ - /// Initialise the events. - /// The core event manager. - internal static void Init(EventManager eventManager) - { - GameEvents.EventManager = eventManager; - } - } -} -#endif diff --git a/src/SMAPI/Events/GraphicsEvents.cs b/src/SMAPI/Events/GraphicsEvents.cs deleted file mode 100644 index 24a16a29..00000000 --- a/src/SMAPI/Events/GraphicsEvents.cs +++ /dev/null @@ -1,120 +0,0 @@ -#if !SMAPI_3_0_STRICT -using System; -using StardewModdingAPI.Framework; -using StardewModdingAPI.Framework.Events; - -namespace StardewModdingAPI.Events -{ - /// Events raised during the game's draw loop, when the game is rendering content to the window. - [Obsolete("Use " + nameof(Mod.Helper) + "." + nameof(IModHelper.Events) + " instead. See https://smapi.io/3.0 for more info.")] - public static class GraphicsEvents - { - /********* - ** Fields - *********/ - /// The core event manager. - private static EventManager EventManager; - - - /********* - ** Events - *********/ - /// Raised after the game window is resized. - public static event EventHandler Resize - { - add - { - SCore.DeprecationManager.WarnForOldEvents(); - GraphicsEvents.EventManager.Legacy_Resize.Add(value); - } - remove => GraphicsEvents.EventManager.Legacy_Resize.Remove(value); - } - - /**** - ** Main render events - ****/ - /// Raised before drawing the world to the screen. - public static event EventHandler OnPreRenderEvent - { - add - { - SCore.DeprecationManager.WarnForOldEvents(); - GraphicsEvents.EventManager.Legacy_OnPreRenderEvent.Add(value); - } - remove => GraphicsEvents.EventManager.Legacy_OnPreRenderEvent.Remove(value); - } - - /// Raised after drawing the world to the screen. - public static event EventHandler OnPostRenderEvent - { - add - { - SCore.DeprecationManager.WarnForOldEvents(); - GraphicsEvents.EventManager.Legacy_OnPostRenderEvent.Add(value); - } - remove => GraphicsEvents.EventManager.Legacy_OnPostRenderEvent.Remove(value); - } - - /**** - ** HUD events - ****/ - /// Raised before drawing the HUD (item toolbar, clock, etc) to the screen. The HUD is available at this point, but not necessarily visible. (For example, the event is raised even if a menu is open.) - public static event EventHandler OnPreRenderHudEvent - { - add - { - SCore.DeprecationManager.WarnForOldEvents(); - GraphicsEvents.EventManager.Legacy_OnPreRenderHudEvent.Add(value); - } - remove => GraphicsEvents.EventManager.Legacy_OnPreRenderHudEvent.Remove(value); - } - - /// Raised after drawing the HUD (item toolbar, clock, etc) to the screen. The HUD is available at this point, but not necessarily visible. (For example, the event is raised even if a menu is open.) - public static event EventHandler OnPostRenderHudEvent - { - add - { - SCore.DeprecationManager.WarnForOldEvents(); - GraphicsEvents.EventManager.Legacy_OnPostRenderHudEvent.Add(value); - } - remove => GraphicsEvents.EventManager.Legacy_OnPostRenderHudEvent.Remove(value); - } - - /**** - ** GUI events - ****/ - /// Raised before drawing a menu to the screen during a draw loop. This includes the game's internal menus like the title screen. - public static event EventHandler OnPreRenderGuiEvent - { - add - { - SCore.DeprecationManager.WarnForOldEvents(); - GraphicsEvents.EventManager.Legacy_OnPreRenderGuiEvent.Add(value); - } - remove => GraphicsEvents.EventManager.Legacy_OnPreRenderGuiEvent.Remove(value); - } - - /// Raised after drawing a menu to the screen during a draw loop. This includes the game's internal menus like the title screen. - public static event EventHandler OnPostRenderGuiEvent - { - add - { - SCore.DeprecationManager.WarnForOldEvents(); - GraphicsEvents.EventManager.Legacy_OnPostRenderGuiEvent.Add(value); - } - remove => GraphicsEvents.EventManager.Legacy_OnPostRenderGuiEvent.Remove(value); - } - - - /********* - ** Public methods - *********/ - /// Initialise the events. - /// The core event manager. - internal static void Init(EventManager eventManager) - { - GraphicsEvents.EventManager = eventManager; - } - } -} -#endif diff --git a/src/SMAPI/Events/InputEvents.cs b/src/SMAPI/Events/InputEvents.cs deleted file mode 100644 index c5ab8c83..00000000 --- a/src/SMAPI/Events/InputEvents.cs +++ /dev/null @@ -1,56 +0,0 @@ -#if !SMAPI_3_0_STRICT -using System; -using StardewModdingAPI.Framework; -using StardewModdingAPI.Framework.Events; - -namespace StardewModdingAPI.Events -{ - /// Events raised when the player uses a controller, keyboard, or mouse button. - [Obsolete("Use " + nameof(Mod.Helper) + "." + nameof(IModHelper.Events) + " instead. See https://smapi.io/3.0 for more info.")] - public static class InputEvents - { - /********* - ** Fields - *********/ - /// The core event manager. - private static EventManager EventManager; - - - /********* - ** Events - *********/ - /// Raised when the player presses a button on the keyboard, controller, or mouse. - public static event EventHandler ButtonPressed - { - add - { - SCore.DeprecationManager.WarnForOldEvents(); - InputEvents.EventManager.Legacy_ButtonPressed.Add(value); - } - remove => InputEvents.EventManager.Legacy_ButtonPressed.Remove(value); - } - - /// Raised when the player releases a keyboard key on the keyboard, controller, or mouse. - public static event EventHandler ButtonReleased - { - add - { - SCore.DeprecationManager.WarnForOldEvents(); - InputEvents.EventManager.Legacy_ButtonReleased.Add(value); - } - remove => InputEvents.EventManager.Legacy_ButtonReleased.Remove(value); - } - - - /********* - ** Public methods - *********/ - /// Initialise the events. - /// The core event manager. - internal static void Init(EventManager eventManager) - { - InputEvents.EventManager = eventManager; - } - } -} -#endif diff --git a/src/SMAPI/Events/LocationEvents.cs b/src/SMAPI/Events/LocationEvents.cs deleted file mode 100644 index 0761bdd8..00000000 --- a/src/SMAPI/Events/LocationEvents.cs +++ /dev/null @@ -1,67 +0,0 @@ -#if !SMAPI_3_0_STRICT -using System; -using StardewModdingAPI.Framework; -using StardewModdingAPI.Framework.Events; - -namespace StardewModdingAPI.Events -{ - /// Events raised when the player transitions between game locations, a location is added or removed, or the objects in the current location change. - [Obsolete("Use " + nameof(Mod.Helper) + "." + nameof(IModHelper.Events) + " instead. See https://smapi.io/3.0 for more info.")] - public static class LocationEvents - { - /********* - ** Fields - *********/ - /// The core event manager. - private static EventManager EventManager; - - - /********* - ** Events - *********/ - /// Raised after a game location is added or removed. - public static event EventHandler LocationsChanged - { - add - { - SCore.DeprecationManager.WarnForOldEvents(); - LocationEvents.EventManager.Legacy_LocationsChanged.Add(value); - } - remove => LocationEvents.EventManager.Legacy_LocationsChanged.Remove(value); - } - - /// Raised after buildings are added or removed in a location. - public static event EventHandler BuildingsChanged - { - add - { - SCore.DeprecationManager.WarnForOldEvents(); - LocationEvents.EventManager.Legacy_BuildingsChanged.Add(value); - } - remove => LocationEvents.EventManager.Legacy_BuildingsChanged.Remove(value); - } - - /// Raised after objects are added or removed in a location. - public static event EventHandler ObjectsChanged - { - add - { - SCore.DeprecationManager.WarnForOldEvents(); - LocationEvents.EventManager.Legacy_ObjectsChanged.Add(value); - } - remove => LocationEvents.EventManager.Legacy_ObjectsChanged.Remove(value); - } - - - /********* - ** Public methods - *********/ - /// Initialise the events. - /// The core event manager. - internal static void Init(EventManager eventManager) - { - LocationEvents.EventManager = eventManager; - } - } -} -#endif diff --git a/src/SMAPI/Events/MenuEvents.cs b/src/SMAPI/Events/MenuEvents.cs deleted file mode 100644 index 8647c268..00000000 --- a/src/SMAPI/Events/MenuEvents.cs +++ /dev/null @@ -1,56 +0,0 @@ -#if !SMAPI_3_0_STRICT -using System; -using StardewModdingAPI.Framework; -using StardewModdingAPI.Framework.Events; - -namespace StardewModdingAPI.Events -{ - /// Events raised when a game menu is opened or closed (including internal menus like the title screen). - [Obsolete("Use " + nameof(Mod.Helper) + "." + nameof(IModHelper.Events) + " instead. See https://smapi.io/3.0 for more info.")] - public static class MenuEvents - { - /********* - ** Fields - *********/ - /// The core event manager. - private static EventManager EventManager; - - - /********* - ** Events - *********/ - /// Raised after a game menu is opened or replaced with another menu. This event is not invoked when a menu is closed. - public static event EventHandler MenuChanged - { - add - { - SCore.DeprecationManager.WarnForOldEvents(); - MenuEvents.EventManager.Legacy_MenuChanged.Add(value); - } - remove => MenuEvents.EventManager.Legacy_MenuChanged.Remove(value); - } - - /// Raised after a game menu is closed. - public static event EventHandler MenuClosed - { - add - { - SCore.DeprecationManager.WarnForOldEvents(); - MenuEvents.EventManager.Legacy_MenuClosed.Add(value); - } - remove => MenuEvents.EventManager.Legacy_MenuClosed.Remove(value); - } - - - /********* - ** Public methods - *********/ - /// Initialise the events. - /// The core event manager. - internal static void Init(EventManager eventManager) - { - MenuEvents.EventManager = eventManager; - } - } -} -#endif diff --git a/src/SMAPI/Events/MineEvents.cs b/src/SMAPI/Events/MineEvents.cs deleted file mode 100644 index 929da35b..00000000 --- a/src/SMAPI/Events/MineEvents.cs +++ /dev/null @@ -1,45 +0,0 @@ -#if !SMAPI_3_0_STRICT -using System; -using StardewModdingAPI.Framework; -using StardewModdingAPI.Framework.Events; - -namespace StardewModdingAPI.Events -{ - /// Events raised when something happens in the mines. - [Obsolete("Use " + nameof(Mod.Helper) + "." + nameof(IModHelper.Events) + " instead. See https://smapi.io/3.0 for more info.")] - public static class MineEvents - { - /********* - ** Fields - *********/ - /// The core event manager. - private static EventManager EventManager; - - - /********* - ** Events - *********/ - /// Raised after the player warps to a new level of the mine. - public static event EventHandler MineLevelChanged - { - add - { - SCore.DeprecationManager.WarnForOldEvents(); - MineEvents.EventManager.Legacy_MineLevelChanged.Add(value); - } - remove => MineEvents.EventManager.Legacy_MineLevelChanged.Remove(value); - } - - - /********* - ** Public methods - *********/ - /// Initialise the events. - /// The core event manager. - internal static void Init(EventManager eventManager) - { - MineEvents.EventManager = eventManager; - } - } -} -#endif diff --git a/src/SMAPI/Events/MultiplayerEvents.cs b/src/SMAPI/Events/MultiplayerEvents.cs deleted file mode 100644 index 0650a8e2..00000000 --- a/src/SMAPI/Events/MultiplayerEvents.cs +++ /dev/null @@ -1,78 +0,0 @@ -#if !SMAPI_3_0_STRICT -using System; -using StardewModdingAPI.Framework; -using StardewModdingAPI.Framework.Events; - -namespace StardewModdingAPI.Events -{ - /// Events raised during the multiplayer sync process. - [Obsolete("Use " + nameof(Mod.Helper) + "." + nameof(IModHelper.Events) + " instead. See https://smapi.io/3.0 for more info.")] - public static class MultiplayerEvents - { - /********* - ** Fields - *********/ - /// The core event manager. - private static EventManager EventManager; - - - /********* - ** Events - *********/ - /// Raised before the game syncs changes from other players. - public static event EventHandler BeforeMainSync - { - add - { - SCore.DeprecationManager.WarnForOldEvents(); - MultiplayerEvents.EventManager.Legacy_BeforeMainSync.Add(value); - } - remove => MultiplayerEvents.EventManager.Legacy_BeforeMainSync.Remove(value); - } - - /// Raised after the game syncs changes from other players. - public static event EventHandler AfterMainSync - { - add - { - SCore.DeprecationManager.WarnForOldEvents(); - MultiplayerEvents.EventManager.Legacy_AfterMainSync.Add(value); - } - remove => MultiplayerEvents.EventManager.Legacy_AfterMainSync.Remove(value); - } - - /// Raised before the game broadcasts changes to other players. - public static event EventHandler BeforeMainBroadcast - { - add - { - SCore.DeprecationManager.WarnForOldEvents(); - MultiplayerEvents.EventManager.Legacy_BeforeMainBroadcast.Add(value); - } - remove => MultiplayerEvents.EventManager.Legacy_BeforeMainBroadcast.Remove(value); - } - - /// Raised after the game broadcasts changes to other players. - public static event EventHandler AfterMainBroadcast - { - add - { - SCore.DeprecationManager.WarnForOldEvents(); - MultiplayerEvents.EventManager.Legacy_AfterMainBroadcast.Add(value); - } - remove => MultiplayerEvents.EventManager.Legacy_AfterMainBroadcast.Remove(value); - } - - - /********* - ** Public methods - *********/ - /// Initialise the events. - /// The core event manager. - internal static void Init(EventManager eventManager) - { - MultiplayerEvents.EventManager = eventManager; - } - } -} -#endif diff --git a/src/SMAPI/Events/PlayerEvents.cs b/src/SMAPI/Events/PlayerEvents.cs deleted file mode 100644 index 11ba1e54..00000000 --- a/src/SMAPI/Events/PlayerEvents.cs +++ /dev/null @@ -1,68 +0,0 @@ -#if !SMAPI_3_0_STRICT -using System; -using StardewModdingAPI.Framework; -using StardewModdingAPI.Framework.Events; - -namespace StardewModdingAPI.Events -{ - /// Events raised when the player data changes. - [Obsolete("Use " + nameof(Mod.Helper) + "." + nameof(IModHelper.Events) + " instead. See https://smapi.io/3.0 for more info.")] - public static class PlayerEvents - { - /********* - ** Fields - *********/ - /// The core event manager. - private static EventManager EventManager; - - - /********* - ** Events - *********/ - /// Raised after the player's inventory changes in any way (added or removed item, sorted, etc). - public static event EventHandler InventoryChanged - { - add - { - SCore.DeprecationManager.WarnForOldEvents(); - PlayerEvents.EventManager.Legacy_InventoryChanged.Add(value); - } - remove => PlayerEvents.EventManager.Legacy_InventoryChanged.Remove(value); - } - - /// Raised after the player levels up a skill. This happens as soon as they level up, not when the game notifies the player after their character goes to bed. - public static event EventHandler LeveledUp - { - add - { - SCore.DeprecationManager.WarnForOldEvents(); - PlayerEvents.EventManager.Legacy_LeveledUp.Add(value); - } - remove => PlayerEvents.EventManager.Legacy_LeveledUp.Remove(value); - } - - /// Raised after the player warps to a new location. - public static event EventHandler Warped - { - add - { - SCore.DeprecationManager.WarnForOldEvents(); - PlayerEvents.EventManager.Legacy_PlayerWarped.Add(value); - } - remove => PlayerEvents.EventManager.Legacy_PlayerWarped.Remove(value); - } - - - - /********* - ** Public methods - *********/ - /// Initialise the events. - /// The core event manager. - internal static void Init(EventManager eventManager) - { - PlayerEvents.EventManager = eventManager; - } - } -} -#endif diff --git a/src/SMAPI/Events/SaveEvents.cs b/src/SMAPI/Events/SaveEvents.cs deleted file mode 100644 index da276d22..00000000 --- a/src/SMAPI/Events/SaveEvents.cs +++ /dev/null @@ -1,100 +0,0 @@ -#if !SMAPI_3_0_STRICT -using System; -using StardewModdingAPI.Framework; -using StardewModdingAPI.Framework.Events; - -namespace StardewModdingAPI.Events -{ - /// Events raised before and after the player saves/loads the game. - [Obsolete("Use " + nameof(Mod.Helper) + "." + nameof(IModHelper.Events) + " instead. See https://smapi.io/3.0 for more info.")] - public static class SaveEvents - { - /********* - ** Fields - *********/ - /// The core event manager. - private static EventManager EventManager; - - - /********* - ** Events - *********/ - /// Raised before the game creates the save file. - public static event EventHandler BeforeCreate - { - add - { - SCore.DeprecationManager.WarnForOldEvents(); - SaveEvents.EventManager.Legacy_BeforeCreateSave.Add(value); - } - remove => SaveEvents.EventManager.Legacy_BeforeCreateSave.Remove(value); - } - - /// Raised after the game finishes creating the save file. - public static event EventHandler AfterCreate - { - add - { - SCore.DeprecationManager.WarnForOldEvents(); - SaveEvents.EventManager.Legacy_AfterCreateSave.Add(value); - } - remove => SaveEvents.EventManager.Legacy_AfterCreateSave.Remove(value); - } - - /// Raised before the game begins writes data to the save file. - public static event EventHandler BeforeSave - { - add - { - SCore.DeprecationManager.WarnForOldEvents(); - SaveEvents.EventManager.Legacy_BeforeSave.Add(value); - } - remove => SaveEvents.EventManager.Legacy_BeforeSave.Remove(value); - } - - /// Raised after the game finishes writing data to the save file. - public static event EventHandler AfterSave - { - add - { - SCore.DeprecationManager.WarnForOldEvents(); - SaveEvents.EventManager.Legacy_AfterSave.Add(value); - } - remove => SaveEvents.EventManager.Legacy_AfterSave.Remove(value); - } - - /// Raised after the player loads a save slot. - public static event EventHandler AfterLoad - { - add - { - SCore.DeprecationManager.WarnForOldEvents(); - SaveEvents.EventManager.Legacy_AfterLoad.Add(value); - } - remove => SaveEvents.EventManager.Legacy_AfterLoad.Remove(value); - } - - /// Raised after the game returns to the title screen. - public static event EventHandler AfterReturnToTitle - { - add - { - SCore.DeprecationManager.WarnForOldEvents(); - SaveEvents.EventManager.Legacy_AfterReturnToTitle.Add(value); - } - remove => SaveEvents.EventManager.Legacy_AfterReturnToTitle.Remove(value); - } - - - /********* - ** Public methods - *********/ - /// Initialise the events. - /// The core event manager. - internal static void Init(EventManager eventManager) - { - SaveEvents.EventManager = eventManager; - } - } -} -#endif diff --git a/src/SMAPI/Events/SpecialisedEvents.cs b/src/SMAPI/Events/SpecialisedEvents.cs deleted file mode 100644 index 4f16e4da..00000000 --- a/src/SMAPI/Events/SpecialisedEvents.cs +++ /dev/null @@ -1,45 +0,0 @@ -#if !SMAPI_3_0_STRICT -using System; -using StardewModdingAPI.Framework; -using StardewModdingAPI.Framework.Events; - -namespace StardewModdingAPI.Events -{ - /// Events serving specialised edge cases that shouldn't be used by most mods. - [Obsolete("Use " + nameof(Mod.Helper) + "." + nameof(IModHelper.Events) + " instead. See https://smapi.io/3.0 for more info.")] - public static class SpecialisedEvents - { - /********* - ** Fields - *********/ - /// The core event manager. - private static EventManager EventManager; - - - /********* - ** Events - *********/ - /// Raised when the game updates its state (≈60 times per second), regardless of normal SMAPI validation. This event is not thread-safe and may be invoked while game logic is running asynchronously. Changes to game state in this method may crash the game or corrupt an in-progress save. Do not use this event unless you're fully aware of the context in which your code will be run. Mods using this method will trigger a stability warning in the SMAPI console. - public static event EventHandler UnvalidatedUpdateTick - { - add - { - SCore.DeprecationManager.WarnForOldEvents(); - SpecialisedEvents.EventManager.Legacy_UnvalidatedUpdateTick.Add(value); - } - remove => SpecialisedEvents.EventManager.Legacy_UnvalidatedUpdateTick.Remove(value); - } - - - /********* - ** Public methods - *********/ - /// Initialise the events. - /// The core event manager. - internal static void Init(EventManager eventManager) - { - SpecialisedEvents.EventManager = eventManager; - } - } -} -#endif diff --git a/src/SMAPI/Events/TimeEvents.cs b/src/SMAPI/Events/TimeEvents.cs deleted file mode 100644 index 389532d9..00000000 --- a/src/SMAPI/Events/TimeEvents.cs +++ /dev/null @@ -1,56 +0,0 @@ -#if !SMAPI_3_0_STRICT -using System; -using StardewModdingAPI.Framework; -using StardewModdingAPI.Framework.Events; - -namespace StardewModdingAPI.Events -{ - /// Events raised when the in-game date or time changes. - [Obsolete("Use " + nameof(Mod.Helper) + "." + nameof(IModHelper.Events) + " instead. See https://smapi.io/3.0 for more info.")] - public static class TimeEvents - { - /********* - ** Fields - *********/ - /// The core event manager. - private static EventManager EventManager; - - - /********* - ** Events - *********/ - /// Raised after the game begins a new day, including when loading a save. - public static event EventHandler AfterDayStarted - { - add - { - SCore.DeprecationManager.WarnForOldEvents(); - TimeEvents.EventManager.Legacy_AfterDayStarted.Add(value); - } - remove => TimeEvents.EventManager.Legacy_AfterDayStarted.Remove(value); - } - - /// Raised after the in-game clock changes. - public static event EventHandler TimeOfDayChanged - { - add - { - SCore.DeprecationManager.WarnForOldEvents(); - TimeEvents.EventManager.Legacy_TimeOfDayChanged.Add(value); - } - remove => TimeEvents.EventManager.Legacy_TimeOfDayChanged.Remove(value); - } - - - /********* - ** Public methods - *********/ - /// Initialise the events. - /// The core event manager. - internal static void Init(EventManager eventManager) - { - TimeEvents.EventManager = eventManager; - } - } -} -#endif diff --git a/src/SMAPI/Framework/Content/AssetDataForDictionary.cs b/src/SMAPI/Framework/Content/AssetDataForDictionary.cs index 11a2564c..a331f38a 100644 --- a/src/SMAPI/Framework/Content/AssetDataForDictionary.cs +++ b/src/SMAPI/Framework/Content/AssetDataForDictionary.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; namespace StardewModdingAPI.Framework.Content { @@ -18,37 +17,5 @@ namespace StardewModdingAPI.Framework.Content /// A callback to invoke when the data is replaced (if any). public AssetDataForDictionary(string locale, string assetName, IDictionary data, Func getNormalisedPath, Action> onDataReplaced) : base(locale, assetName, data, getNormalisedPath, onDataReplaced) { } - -#if !SMAPI_3_0_STRICT - /// Add or replace an entry in the dictionary. - /// The entry key. - /// The entry value. - [Obsolete("Access " + nameof(AssetData>.Data) + "field directly.")] - public void Set(TKey key, TValue value) - { - SCore.DeprecationManager.Warn($"AssetDataForDictionary.{nameof(Set)}", "2.10", DeprecationLevel.PendingRemoval); - this.Data[key] = value; - } - - /// Add or replace an entry in the dictionary. - /// The entry key. - /// A callback which accepts the current value and returns the new value. - [Obsolete("Access " + nameof(AssetData>.Data) + "field directly.")] - public void Set(TKey key, Func value) - { - SCore.DeprecationManager.Warn($"AssetDataForDictionary.{nameof(Set)}", "2.10", DeprecationLevel.PendingRemoval); - this.Data[key] = value(this.Data[key]); - } - - /// Dynamically replace values in the dictionary. - /// A lambda which takes the current key and value for an entry, and returns the new value. - [Obsolete("Access " + nameof(AssetData>.Data) + "field directly.")] - public void Set(Func replacer) - { - SCore.DeprecationManager.Warn($"AssetDataForDictionary.{nameof(Set)}", "2.10", DeprecationLevel.PendingRemoval); - foreach (var pair in this.Data.ToArray()) - this.Data[pair.Key] = replacer(pair.Key, pair.Value); - } -#endif } } diff --git a/src/SMAPI/Framework/DeprecationManager.cs b/src/SMAPI/Framework/DeprecationManager.cs index 984bb487..636b1979 100644 --- a/src/SMAPI/Framework/DeprecationManager.cs +++ b/src/SMAPI/Framework/DeprecationManager.cs @@ -14,11 +14,7 @@ namespace StardewModdingAPI.Framework private readonly HashSet LoggedDeprecations = new HashSet(StringComparer.InvariantCultureIgnoreCase); /// Encapsulates monitoring and logging for a given module. -#if !SMAPI_3_0_STRICT - private readonly Monitor Monitor; -#else private readonly IMonitor Monitor; -#endif /// Tracks the installed mods. private readonly ModRegistry ModRegistry; @@ -26,11 +22,6 @@ namespace StardewModdingAPI.Framework /// The queued deprecation warnings to display. private readonly IList QueuedWarnings = new List(); -#if !SMAPI_3_0_STRICT - /// Whether the one-time deprecation message has been shown. - private bool DeprecationHeaderShown = false; -#endif - /********* ** Public methods @@ -38,11 +29,7 @@ namespace StardewModdingAPI.Framework /// Construct an instance. /// Encapsulates monitoring and logging for a given module. /// Tracks the installed mods. -#if !SMAPI_3_0_STRICT - public DeprecationManager(Monitor monitor, ModRegistry modRegistry) -#else public DeprecationManager(IMonitor monitor, ModRegistry modRegistry) -#endif { this.Monitor = monitor; this.ModRegistry = modRegistry; @@ -81,26 +68,10 @@ namespace StardewModdingAPI.Framework /// Print any queued messages. public void PrintQueued() { -#if !SMAPI_3_0_STRICT - if (!this.DeprecationHeaderShown && this.QueuedWarnings.Any()) - { - this.Monitor.Newline(); - this.Monitor.Log("Some of your mods will break in the upcoming SMAPI 3.0. Please update your mods now, or notify the author if no update is available. See https://mods.smapi.io for links to the latest versions.", LogLevel.Warn); - this.Monitor.Newline(); - this.DeprecationHeaderShown = true; - } -#endif - foreach (DeprecationWarning warning in this.QueuedWarnings.OrderBy(p => p.ModName).ThenBy(p => p.NounPhrase)) { // build message -#if SMAPI_3_0_STRICT string message = $"{warning.ModName} uses deprecated code ({warning.NounPhrase} is deprecated since SMAPI {warning.Version})."; -#else - string message = warning.NounPhrase == "legacy events" - ? $"{warning.ModName ?? "An unknown mod"} will break in the upcoming SMAPI 3.0 (legacy events are deprecated since SMAPI {warning.Version})." - : $"{warning.ModName ?? "An unknown mod"} will break in the upcoming SMAPI 3.0 ({warning.NounPhrase} is deprecated since SMAPI {warning.Version})."; -#endif // get log level LogLevel level; diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 13244601..23879f1d 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -1,7 +1,4 @@ using System.Diagnostics.CodeAnalysis; -#if !SMAPI_3_0_STRICT -using Microsoft.Xna.Framework.Input; -#endif using StardewModdingAPI.Events; namespace StardewModdingAPI.Framework.Events @@ -167,196 +164,6 @@ namespace StardewModdingAPI.Framework.Events public readonly ManagedEvent UnvalidatedUpdateTicked; -#if !SMAPI_3_0_STRICT - /********* - ** Events (old) - *********/ - /**** - ** ContentEvents - ****/ - /// Raised after the content language changes. - public readonly ManagedEvent> Legacy_LocaleChanged; - - /**** - ** ControlEvents - ****/ - /// Raised when the changes. That happens when the player presses or releases a key. - public readonly ManagedEvent Legacy_KeyboardChanged; - - /// Raised after the player presses a keyboard key. - public readonly ManagedEvent Legacy_KeyPressed; - - /// Raised after the player releases a keyboard key. - public readonly ManagedEvent Legacy_KeyReleased; - - /// Raised when the changes. That happens when the player moves the mouse, scrolls the mouse wheel, or presses/releases a button. - public readonly ManagedEvent Legacy_MouseChanged; - - /// The player pressed a controller button. This event isn't raised for trigger buttons. - public readonly ManagedEvent Legacy_ControllerButtonPressed; - - /// The player released a controller button. This event isn't raised for trigger buttons. - public readonly ManagedEvent Legacy_ControllerButtonReleased; - - /// The player pressed a controller trigger button. - public readonly ManagedEvent Legacy_ControllerTriggerPressed; - - /// The player released a controller trigger button. - public readonly ManagedEvent Legacy_ControllerTriggerReleased; - - /**** - ** GameEvents - ****/ - /// Raised once after the game initialises and all methods have been called. - public readonly ManagedEvent Legacy_FirstUpdateTick; - - /// Raised when the game updates its state (≈60 times per second). - public readonly ManagedEvent Legacy_UpdateTick; - - /// Raised every other tick (≈30 times per second). - public readonly ManagedEvent Legacy_SecondUpdateTick; - - /// Raised every fourth tick (≈15 times per second). - public readonly ManagedEvent Legacy_FourthUpdateTick; - - /// Raised every eighth tick (≈8 times per second). - public readonly ManagedEvent Legacy_EighthUpdateTick; - - /// Raised every 15th tick (≈4 times per second). - public readonly ManagedEvent Legacy_QuarterSecondTick; - - /// Raised every 30th tick (≈twice per second). - public readonly ManagedEvent Legacy_HalfSecondTick; - - /// Raised every 60th tick (≈once per second). - public readonly ManagedEvent Legacy_OneSecondTick; - - /**** - ** GraphicsEvents - ****/ - /// Raised after the game window is resized. - public readonly ManagedEvent Legacy_Resize; - - /// Raised before drawing the world to the screen. - public readonly ManagedEvent Legacy_OnPreRenderEvent; - - /// Raised after drawing the world to the screen. - public readonly ManagedEvent Legacy_OnPostRenderEvent; - - /// Raised before drawing the HUD (item toolbar, clock, etc) to the screen. The HUD is available at this point, but not necessarily visible. (For example, the event is raised even if a menu is open.) - public readonly ManagedEvent Legacy_OnPreRenderHudEvent; - - /// Raised after drawing the HUD (item toolbar, clock, etc) to the screen. The HUD is available at this point, but not necessarily visible. (For example, the event is raised even if a menu is open.) - public readonly ManagedEvent Legacy_OnPostRenderHudEvent; - - /// Raised before drawing a menu to the screen during a draw loop. This includes the game's internal menus like the title screen. - public readonly ManagedEvent Legacy_OnPreRenderGuiEvent; - - /// Raised after drawing a menu to the screen during a draw loop. This includes the game's internal menus like the title screen. - public readonly ManagedEvent Legacy_OnPostRenderGuiEvent; - - /**** - ** InputEvents - ****/ - /// Raised after the player presses a button on the keyboard, controller, or mouse. - public readonly ManagedEvent Legacy_ButtonPressed; - - /// Raised after the player releases a keyboard key on the keyboard, controller, or mouse. - public readonly ManagedEvent Legacy_ButtonReleased; - - /**** - ** LocationEvents - ****/ - /// Raised after a game location is added or removed. - public readonly ManagedEvent Legacy_LocationsChanged; - - /// Raised after buildings are added or removed in a location. - public readonly ManagedEvent Legacy_BuildingsChanged; - - /// Raised after objects are added or removed in a location. - public readonly ManagedEvent Legacy_ObjectsChanged; - - /**** - ** MenuEvents - ****/ - /// Raised after a game menu is opened or replaced with another menu. This event is not invoked when a menu is closed. - public readonly ManagedEvent Legacy_MenuChanged; - - /// Raised after a game menu is closed. - public readonly ManagedEvent Legacy_MenuClosed; - - /**** - ** MultiplayerEvents - ****/ - /// Raised before the game syncs changes from other players. - public readonly ManagedEvent Legacy_BeforeMainSync; - - /// Raised after the game syncs changes from other players. - public readonly ManagedEvent Legacy_AfterMainSync; - - /// Raised before the game broadcasts changes to other players. - public readonly ManagedEvent Legacy_BeforeMainBroadcast; - - /// Raised after the game broadcasts changes to other players. - public readonly ManagedEvent Legacy_AfterMainBroadcast; - - /**** - ** MineEvents - ****/ - /// Raised after the player warps to a new level of the mine. - public readonly ManagedEvent Legacy_MineLevelChanged; - - /**** - ** PlayerEvents - ****/ - /// Raised after the player's inventory changes in any way (added or removed item, sorted, etc). - public readonly ManagedEvent Legacy_InventoryChanged; - - /// Raised after the player levels up a skill. This happens as soon as they level up, not when the game notifies the player after their character goes to bed. - public readonly ManagedEvent Legacy_LeveledUp; - - /// Raised after the player warps to a new location. - public readonly ManagedEvent Legacy_PlayerWarped; - - - /**** - ** SaveEvents - ****/ - /// Raised before the game creates the save file. - public readonly ManagedEvent Legacy_BeforeCreateSave; - - /// Raised after the game finishes creating the save file. - public readonly ManagedEvent Legacy_AfterCreateSave; - - /// Raised before the game begins writes data to the save file. - public readonly ManagedEvent Legacy_BeforeSave; - - /// Raised after the game finishes writing data to the save file. - public readonly ManagedEvent Legacy_AfterSave; - - /// Raised after the player loads a save slot. - public readonly ManagedEvent Legacy_AfterLoad; - - /// Raised after the game returns to the title screen. - public readonly ManagedEvent Legacy_AfterReturnToTitle; - - /**** - ** SpecialisedEvents - ****/ - /// Raised when the game updates its state (≈60 times per second), regardless of normal SMAPI validation. This event is not thread-safe and may be invoked while game logic is running asynchronously. Changes to game state in this method may crash the game or corrupt an in-progress save. Do not use this event unless you're fully aware of the context in which your code will be run. Mods using this method will trigger a stability warning in the SMAPI console. - public readonly ManagedEvent Legacy_UnvalidatedUpdateTick; - - /**** - ** TimeEvents - ****/ - /// Raised after the game begins a new day, including when loading a save. - public readonly ManagedEvent Legacy_AfterDayStarted; - - /// Raised after the in-game clock changes. - public readonly ManagedEvent Legacy_TimeOfDayChanged; -#endif - - /********* ** Public methods *********/ @@ -367,9 +174,6 @@ namespace StardewModdingAPI.Framework.Events { // create shortcut initialisers ManagedEvent ManageEventOf(string typeName, string eventName) => new ManagedEvent($"{typeName}.{eventName}", monitor, modRegistry); -#if !SMAPI_3_0_STRICT - ManagedEvent ManageEvent(string typeName, string eventName) => new ManagedEvent($"{typeName}.{eventName}", monitor, modRegistry); -#endif // init events (new) this.MenuChanged = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.MenuChanged)); @@ -422,70 +226,6 @@ namespace StardewModdingAPI.Framework.Events this.LoadStageChanged = ManageEventOf(nameof(IModEvents.Specialised), nameof(ISpecialisedEvents.LoadStageChanged)); this.UnvalidatedUpdateTicking = ManageEventOf(nameof(IModEvents.Specialised), nameof(ISpecialisedEvents.UnvalidatedUpdateTicking)); this.UnvalidatedUpdateTicked = ManageEventOf(nameof(IModEvents.Specialised), nameof(ISpecialisedEvents.UnvalidatedUpdateTicked)); - -#if !SMAPI_3_0_STRICT - // init events (old) - this.Legacy_LocaleChanged = ManageEventOf>(nameof(ContentEvents), nameof(ContentEvents.AfterLocaleChanged)); - - this.Legacy_ControllerButtonPressed = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerButtonPressed)); - this.Legacy_ControllerButtonReleased = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerButtonReleased)); - this.Legacy_ControllerTriggerPressed = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerTriggerPressed)); - this.Legacy_ControllerTriggerReleased = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.ControllerTriggerReleased)); - this.Legacy_KeyboardChanged = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.KeyboardChanged)); - this.Legacy_KeyPressed = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.KeyPressed)); - this.Legacy_KeyReleased = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.KeyReleased)); - this.Legacy_MouseChanged = ManageEventOf(nameof(ControlEvents), nameof(ControlEvents.MouseChanged)); - - this.Legacy_FirstUpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.FirstUpdateTick)); - this.Legacy_UpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.UpdateTick)); - this.Legacy_SecondUpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.SecondUpdateTick)); - this.Legacy_FourthUpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.FourthUpdateTick)); - this.Legacy_EighthUpdateTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.EighthUpdateTick)); - this.Legacy_QuarterSecondTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.QuarterSecondTick)); - this.Legacy_HalfSecondTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.HalfSecondTick)); - this.Legacy_OneSecondTick = ManageEvent(nameof(GameEvents), nameof(GameEvents.OneSecondTick)); - - this.Legacy_Resize = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.Resize)); - this.Legacy_OnPreRenderEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPreRenderEvent)); - this.Legacy_OnPostRenderEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPostRenderEvent)); - this.Legacy_OnPreRenderHudEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPreRenderHudEvent)); - this.Legacy_OnPostRenderHudEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPostRenderHudEvent)); - this.Legacy_OnPreRenderGuiEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPreRenderGuiEvent)); - this.Legacy_OnPostRenderGuiEvent = ManageEvent(nameof(GraphicsEvents), nameof(GraphicsEvents.OnPostRenderGuiEvent)); - - this.Legacy_ButtonPressed = ManageEventOf(nameof(InputEvents), nameof(InputEvents.ButtonPressed)); - this.Legacy_ButtonReleased = ManageEventOf(nameof(InputEvents), nameof(InputEvents.ButtonReleased)); - - this.Legacy_LocationsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.LocationsChanged)); - this.Legacy_BuildingsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.BuildingsChanged)); - this.Legacy_ObjectsChanged = ManageEventOf(nameof(LocationEvents), nameof(LocationEvents.ObjectsChanged)); - - this.Legacy_MenuChanged = ManageEventOf(nameof(MenuEvents), nameof(MenuEvents.MenuChanged)); - this.Legacy_MenuClosed = ManageEventOf(nameof(MenuEvents), nameof(MenuEvents.MenuClosed)); - - this.Legacy_BeforeMainBroadcast = ManageEvent(nameof(MultiplayerEvents), nameof(MultiplayerEvents.BeforeMainBroadcast)); - this.Legacy_AfterMainBroadcast = ManageEvent(nameof(MultiplayerEvents), nameof(MultiplayerEvents.AfterMainBroadcast)); - this.Legacy_BeforeMainSync = ManageEvent(nameof(MultiplayerEvents), nameof(MultiplayerEvents.BeforeMainSync)); - this.Legacy_AfterMainSync = ManageEvent(nameof(MultiplayerEvents), nameof(MultiplayerEvents.AfterMainSync)); - - this.Legacy_MineLevelChanged = ManageEventOf(nameof(MineEvents), nameof(MineEvents.MineLevelChanged)); - - this.Legacy_InventoryChanged = ManageEventOf(nameof(PlayerEvents), nameof(PlayerEvents.InventoryChanged)); - this.Legacy_LeveledUp = ManageEventOf(nameof(PlayerEvents), nameof(PlayerEvents.LeveledUp)); - this.Legacy_PlayerWarped = ManageEventOf(nameof(PlayerEvents), nameof(PlayerEvents.Warped)); - - this.Legacy_BeforeCreateSave = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.BeforeCreate)); - this.Legacy_AfterCreateSave = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.AfterCreate)); - this.Legacy_BeforeSave = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.BeforeSave)); - this.Legacy_AfterSave = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.AfterSave)); - this.Legacy_AfterLoad = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.AfterLoad)); - this.Legacy_AfterReturnToTitle = ManageEvent(nameof(SaveEvents), nameof(SaveEvents.AfterReturnToTitle)); - - this.Legacy_UnvalidatedUpdateTick = ManageEvent(nameof(SpecialisedEvents), nameof(SpecialisedEvents.UnvalidatedUpdateTick)); - - this.Legacy_AfterDayStarted = ManageEvent(nameof(TimeEvents), nameof(TimeEvents.AfterDayStarted)); - this.Legacy_TimeOfDayChanged = ManageEventOf(nameof(TimeEvents), nameof(TimeEvents.TimeOfDayChanged)); -#endif } } } diff --git a/src/SMAPI/Framework/Events/ManagedEvent.cs b/src/SMAPI/Framework/Events/ManagedEvent.cs index f9e7f6ec..2afe7a03 100644 --- a/src/SMAPI/Framework/Events/ManagedEvent.cs +++ b/src/SMAPI/Framework/Events/ManagedEvent.cs @@ -1,11 +1,12 @@ using System; +using System.Collections.Generic; using System.Linq; namespace StardewModdingAPI.Framework.Events { /// An event wrapper which intercepts and logs errors in handler code. /// The event arguments type. - internal class ManagedEvent : ManagedEventBase> + internal class ManagedEvent { /********* ** Fields @@ -13,6 +14,21 @@ namespace StardewModdingAPI.Framework.Events /// The underlying event. private event EventHandler Event; + /// A human-readable name for the event. + private readonly string EventName; + + /// Writes messages to the log. + private readonly IMonitor Monitor; + + /// The mod registry with which to identify mods. + protected readonly ModRegistry ModRegistry; + + /// The display names for the mods which added each delegate. + private readonly IDictionary, IModMetadata> SourceMods = new Dictionary, IModMetadata>(); + + /// The cached invocation list. + private EventHandler[] CachedInvocationList; + /********* ** Public methods @@ -22,7 +38,17 @@ namespace StardewModdingAPI.Framework.Events /// Writes messages to the log. /// The mod registry with which to identify mods. public ManagedEvent(string eventName, IMonitor monitor, ModRegistry modRegistry) - : base(eventName, monitor, modRegistry) { } + { + this.EventName = eventName; + this.Monitor = monitor; + this.ModRegistry = modRegistry; + } + + /// Get whether anything is listening to the event. + public bool HasListeners() + { + return this.CachedInvocationList?.Length > 0; + } /// Add an event handler. /// The event handler. @@ -91,71 +117,50 @@ namespace StardewModdingAPI.Framework.Events } } } - } - -#if !SMAPI_3_0_STRICT - /// An event wrapper which intercepts and logs errors in handler code. - internal class ManagedEvent : ManagedEventBase - { - /********* - ** Fields - *********/ - /// The underlying event. - private event EventHandler Event; /********* - ** Public methods + ** Private methods *********/ - /// Construct an instance. - /// A human-readable name for the event. - /// Writes messages to the log. - /// The mod registry with which to identify mods. - public ManagedEvent(string eventName, IMonitor monitor, ModRegistry modRegistry) - : base(eventName, monitor, modRegistry) { } - - /// Add an event handler. + /// Track an event handler. + /// The mod which added the handler. /// The event handler. - public void Add(EventHandler handler) + /// The updated event invocation list. + protected void AddTracking(IModMetadata mod, EventHandler handler, IEnumerable> invocationList) { - this.Add(handler, this.ModRegistry.GetFromStack()); + this.SourceMods[handler] = mod; + this.CachedInvocationList = invocationList?.ToArray() ?? new EventHandler[0]; } - /// Add an event handler. + /// Remove tracking for an event handler. /// The event handler. - /// The mod which added the event handler. - public void Add(EventHandler handler, IModMetadata mod) + /// The updated event invocation list. + protected void RemoveTracking(EventHandler handler, IEnumerable> invocationList) { - this.Event += handler; - this.AddTracking(mod, handler, this.Event?.GetInvocationList().Cast()); + this.CachedInvocationList = invocationList?.ToArray() ?? new EventHandler[0]; + if (!this.CachedInvocationList.Contains(handler)) // don't remove if there's still a reference to the removed handler (e.g. it was added twice and removed once) + this.SourceMods.Remove(handler); } - /// Remove an event handler. + /// Get the mod which registered the given event handler, if available. /// The event handler. - public void Remove(EventHandler handler) + protected IModMetadata GetSourceMod(EventHandler handler) { - this.Event -= handler; - this.RemoveTracking(handler, this.Event?.GetInvocationList().Cast()); + return this.SourceMods.TryGetValue(handler, out IModMetadata mod) + ? mod + : null; } - /// Raise the event and notify all handlers. - public void Raise() + /// Log an exception from an event handler. + /// The event handler instance. + /// The exception that was raised. + protected void LogError(EventHandler handler, Exception ex) { - if (this.Event == null) - return; - - foreach (EventHandler handler in this.CachedInvocationList) - { - try - { - handler.Invoke(null, EventArgs.Empty); - } - catch (Exception ex) - { - this.LogError(handler, ex); - } - } + IModMetadata mod = this.GetSourceMod(handler); + if (mod != null) + mod.LogAsMod($"This mod failed in the {this.EventName} event. Technical details: \n{ex.GetLogSummary()}", LogLevel.Error); + else + this.Monitor.Log($"A mod failed in the {this.EventName} event. Technical details: \n{ex.GetLogSummary()}", LogLevel.Error); } } -#endif } diff --git a/src/SMAPI/Framework/Events/ManagedEventBase.cs b/src/SMAPI/Framework/Events/ManagedEventBase.cs deleted file mode 100644 index c8c3516b..00000000 --- a/src/SMAPI/Framework/Events/ManagedEventBase.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace StardewModdingAPI.Framework.Events -{ - /// The base implementation for an event wrapper which intercepts and logs errors in handler code. - internal abstract class ManagedEventBase - { - /********* - ** Fields - *********/ - /// A human-readable name for the event. - private readonly string EventName; - - /// Writes messages to the log. - private readonly IMonitor Monitor; - - /// The mod registry with which to identify mods. - protected readonly ModRegistry ModRegistry; - - /// The display names for the mods which added each delegate. - private readonly IDictionary SourceMods = new Dictionary(); - - /// The cached invocation list. - protected TEventHandler[] CachedInvocationList { get; private set; } - - - /********* - ** Public methods - *********/ - /// Get whether anything is listening to the event. - public bool HasListeners() - { - return this.CachedInvocationList?.Length > 0; - } - - /********* - ** Protected methods - *********/ - /// Construct an instance. - /// A human-readable name for the event. - /// Writes messages to the log. - /// The mod registry with which to identify mods. - protected ManagedEventBase(string eventName, IMonitor monitor, ModRegistry modRegistry) - { - this.EventName = eventName; - this.Monitor = monitor; - this.ModRegistry = modRegistry; - } - - /// Track an event handler. - /// The mod which added the handler. - /// The event handler. - /// The updated event invocation list. - protected void AddTracking(IModMetadata mod, TEventHandler handler, IEnumerable invocationList) - { - this.SourceMods[handler] = mod; - this.CachedInvocationList = invocationList?.ToArray() ?? new TEventHandler[0]; - } - - /// Remove tracking for an event handler. - /// The event handler. - /// The updated event invocation list. - protected void RemoveTracking(TEventHandler handler, IEnumerable invocationList) - { - this.CachedInvocationList = invocationList?.ToArray() ?? new TEventHandler[0]; - if (!this.CachedInvocationList.Contains(handler)) // don't remove if there's still a reference to the removed handler (e.g. it was added twice and removed once) - this.SourceMods.Remove(handler); - } - - /// Get the mod which registered the given event handler, if available. - /// The event handler. - protected IModMetadata GetSourceMod(TEventHandler handler) - { - return this.SourceMods.TryGetValue(handler, out IModMetadata mod) - ? mod - : null; - } - - /// Log an exception from an event handler. - /// The event handler instance. - /// The exception that was raised. - protected void LogError(TEventHandler handler, Exception ex) - { - IModMetadata mod = this.GetSourceMod(handler); - if (mod != null) - mod.LogAsMod($"This mod failed in the {this.EventName} event. Technical details: \n{ex.GetLogSummary()}", LogLevel.Error); - else - this.Monitor.Log($"A mod failed in the {this.EventName} event. Technical details: \n{ex.GetLogSummary()}", LogLevel.Error); - } - } -} diff --git a/src/SMAPI/Framework/ModHelpers/ModHelper.cs b/src/SMAPI/Framework/ModHelpers/ModHelper.cs index 6c9838c9..86e8eb28 100644 --- a/src/SMAPI/Framework/ModHelpers/ModHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModHelper.cs @@ -1,11 +1,8 @@ using System; -using System.Collections.Generic; using System.IO; using StardewModdingAPI.Events; using StardewModdingAPI.Framework.Input; using StardewModdingAPI.Toolkit.Serialisation; -using StardewModdingAPI.Toolkit.Serialisation.Models; -using StardewModdingAPI.Toolkit.Utilities; namespace StardewModdingAPI.Framework.ModHelpers { @@ -18,11 +15,6 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The full path to the mod's folder. public string DirectoryPath { get; } -#if !SMAPI_3_0_STRICT - /// Encapsulates SMAPI's JSON file parsing. - private readonly JsonHelper JsonHelper; -#endif - /// Manages access to events raised by SMAPI, which let your mod react when something happens in the game. public IModEvents Events { get; } @@ -60,7 +52,6 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Construct an instance. /// The mod's unique ID. /// The full path to the mod's folder. - /// Encapsulate SMAPI's JSON parsing. /// Manages the game's input state. /// Manages access to events raised by SMAPI. /// An API for loading content assets. @@ -73,7 +64,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// An API for reading translations stored in the mod's i18n folder. /// An argument is null or empty. /// The path does not exist on disk. - public ModHelper(string modID, string modDirectory, JsonHelper jsonHelper, SInputState inputState, IModEvents events, IContentHelper contentHelper, IContentPackHelper contentPackHelper, ICommandHelper commandHelper, IDataHelper dataHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, IMultiplayerHelper multiplayer, ITranslationHelper translationHelper) + public ModHelper(string modID, string modDirectory, SInputState inputState, IModEvents events, IContentHelper contentHelper, IContentPackHelper contentPackHelper, ICommandHelper commandHelper, IDataHelper dataHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, IMultiplayerHelper multiplayer, ITranslationHelper translationHelper) : base(modID) { // validate directory @@ -94,9 +85,6 @@ namespace StardewModdingAPI.Framework.ModHelpers this.Multiplayer = multiplayer ?? throw new ArgumentNullException(nameof(multiplayer)); this.Translation = translationHelper ?? throw new ArgumentNullException(nameof(translationHelper)); this.Events = events; -#if !SMAPI_3_0_STRICT - this.JsonHelper = jsonHelper ?? throw new ArgumentNullException(nameof(jsonHelper)); -#endif } /**** @@ -121,63 +109,6 @@ namespace StardewModdingAPI.Framework.ModHelpers this.Data.WriteJsonFile("config.json", config); } -#if !SMAPI_3_0_STRICT - /**** - ** Generic JSON files - ****/ - /// Read a JSON file. - /// The model type. - /// The file path relative to the mod directory. - /// Returns the deserialised model, or null if the file doesn't exist or is empty. - [Obsolete("Use " + nameof(ModHelper.Data) + "." + nameof(IDataHelper.ReadJsonFile) + " instead")] - public TModel ReadJsonFile(string path) - where TModel : class - { - path = Path.Combine(this.DirectoryPath, PathUtilities.NormalisePathSeparators(path)); - return this.JsonHelper.ReadJsonFileIfExists(path, out TModel data) - ? data - : null; - } - - /// Save to a JSON file. - /// The model type. - /// The file path relative to the mod directory. - /// The model to save. - [Obsolete("Use " + nameof(ModHelper.Data) + "." + nameof(IDataHelper.WriteJsonFile) + " instead")] - public void WriteJsonFile(string path, TModel model) - where TModel : class - { - path = Path.Combine(this.DirectoryPath, PathUtilities.NormalisePathSeparators(path)); - this.JsonHelper.WriteJsonFile(path, model); - } -#endif - - /**** - ** Content packs - ****/ -#if !SMAPI_3_0_STRICT - /// Manually create a transitional content pack to support pre-SMAPI content packs. This provides a way to access legacy content packs using the SMAPI content pack APIs, but the content pack will not be visible in the log or validated by SMAPI. - /// The absolute directory path containing the content pack files. - /// The content pack's unique ID. - /// The content pack name. - /// The content pack description. - /// The content pack author's name. - /// The content pack version. - [Obsolete("Use " + nameof(IModHelper) + "." + nameof(IModHelper.ContentPacks) + "." + nameof(IContentPackHelper.CreateTemporary) + " instead")] - public IContentPack CreateTransitionalContentPack(string directoryPath, string id, string name, string description, string author, ISemanticVersion version) - { - SCore.DeprecationManager.Warn($"{nameof(IModHelper)}.{nameof(IModHelper.CreateTransitionalContentPack)}", "2.5", DeprecationLevel.PendingRemoval); - return this.ContentPacks.CreateTemporary(directoryPath, id, name, description, author, version); - } - - /// Get all content packs loaded for this mod. - [Obsolete("Use " + nameof(IModHelper) + "." + nameof(IModHelper.ContentPacks) + "." + nameof(IContentPackHelper.GetOwned) + " instead")] - public IEnumerable GetContentPacks() - { - return this.ContentPacks.GetOwned(); - } -#endif - /**** ** Disposal ****/ diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 75d3849d..f2002530 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -147,12 +147,8 @@ namespace StardewModdingAPI.Framework.ModLoading string actualFilename = new DirectoryInfo(mod.DirectoryPath).GetFiles(mod.Manifest.EntryDll).FirstOrDefault()?.Name; if (actualFilename != mod.Manifest.EntryDll) { -#if SMAPI_3_0_STRICT mod.SetStatus(ModMetadataStatus.Failed, $"its {nameof(IManifest.EntryDll)} value '{mod.Manifest.EntryDll}' doesn't match the actual file capitalisation '{actualFilename}'. The capitalisation must match for crossplatform compatibility."); continue; -#else - SCore.DeprecationManager.Warn(mod.DisplayName, $"{nameof(IManifest.EntryDll)} value with case-insensitive capitalisation", "2.11", DeprecationLevel.PendingRemoval); -#endif } } diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 5dd52992..06a2e0af 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -190,24 +190,6 @@ namespace StardewModdingAPI.Framework // initialise SMAPI try { -#if !SMAPI_3_0_STRICT - // hook up events - ContentEvents.Init(this.EventManager); - ControlEvents.Init(this.EventManager); - GameEvents.Init(this.EventManager); - GraphicsEvents.Init(this.EventManager); - InputEvents.Init(this.EventManager); - LocationEvents.Init(this.EventManager); - MenuEvents.Init(this.EventManager); - MineEvents.Init(this.EventManager); - MultiplayerEvents.Init(this.EventManager); - PlayerEvents.Init(this.EventManager); - SaveEvents.Init(this.EventManager); - SpecialisedEvents.Init(this.EventManager); - TimeEvents.Init(this.EventManager); -#endif - - // init JSON parser JsonConverter[] converters = { new ColorConverter(), new PointConverter(), @@ -262,10 +244,6 @@ namespace StardewModdingAPI.Framework // set window titles this.GameInstance.Window.Title = $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion}"; Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion}"; -#if SMAPI_3_0_STRICT - this.GameInstance.Window.Title += " [SMAPI 3.0 strict mode]"; - Console.Title += " [SMAPI 3.0 strict mode]"; -#endif } catch (Exception ex) { @@ -375,9 +353,6 @@ namespace StardewModdingAPI.Framework private void InitialiseAfterGameStart() { // add headers -#if SMAPI_3_0_STRICT - this.Monitor.Log($"You're running SMAPI 3.0 strict mode, so most mods won't work correctly. If that wasn't intended, install the normal version of SMAPI from https://smapi.io instead.", LogLevel.Warn); -#endif if (this.Settings.DeveloperMode) this.Monitor.Log($"You have SMAPI for developers, so the console will be much more verbose. You can disable developer mode by installing the non-developer version of SMAPI, or by editing {Constants.ApiConfigPath}.", LogLevel.Info); if (!this.Settings.CheckForUpdates) @@ -439,11 +414,6 @@ namespace StardewModdingAPI.Framework int modsLoaded = this.ModRegistry.GetAll().Count(); this.GameInstance.Window.Title = $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion} with {modsLoaded} mods"; Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion} with {modsLoaded} mods"; -#if SMAPI_3_0_STRICT - this.GameInstance.Window.Title += " [SMAPI 3.0 strict mode]"; - Console.Title += " [SMAPI 3.0 strict mode]"; -#endif - // start SMAPI console new Thread(this.RunConsoleLoop).Start(); @@ -926,14 +896,6 @@ namespace StardewModdingAPI.Framework return false; } -#if !SMAPI_3_0_STRICT - // add deprecation warning for old version format - { - if (mod.Manifest?.Version is Toolkit.SemanticVersion version && version.IsLegacyFormat) - SCore.DeprecationManager.Warn(mod.DisplayName, "non-string manifest version", "2.8", DeprecationLevel.PendingRemoval); - } -#endif - // validate dependencies // Although dependences are validated before mods are loaded, a dependency may have failed to load. if (mod.Manifest.Dependencies?.Any() == true) @@ -1039,7 +1001,7 @@ namespace StardewModdingAPI.Framework return new ContentPack(packDirPath, packManifest, packContentHelper, this.Toolkit.JsonHelper); } - modHelper = new ModHelper(manifest.UniqueID, mod.DirectoryPath, this.Toolkit.JsonHelper, this.GameInstance.Input, events, contentHelper, contentPackHelper, commandHelper, dataHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper); + modHelper = new ModHelper(manifest.UniqueID, mod.DirectoryPath, this.GameInstance.Input, events, contentHelper, contentPackHelper, commandHelper, dataHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper); } // init mod diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 704eb6bc..b35e1d71 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -9,9 +9,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; -#if !SMAPI_3_0_STRICT -using Microsoft.Xna.Framework.Input; -#endif using Netcode; using StardewModdingAPI.Enums; using StardewModdingAPI.Events; @@ -228,12 +225,7 @@ namespace StardewModdingAPI.Framework // raise events this.Events.LoadStageChanged.Raise(new LoadStageChangedEventArgs(oldStage, newStage)); if (newStage == LoadStage.None) - { this.Events.ReturnedToTitle.RaiseEmpty(); -#if !SMAPI_3_0_STRICT - this.Events.Legacy_AfterReturnToTitle.Raise(); -#endif - } } /// Constructor a content manager to read XNB files. @@ -344,9 +336,6 @@ namespace StardewModdingAPI.Framework SGame.TicksElapsed++; base.Update(gameTime); events.UnvalidatedUpdateTicked.RaiseEmpty(); -#if !SMAPI_3_0_STRICT - events.Legacy_UnvalidatedUpdateTick.Raise(); -#endif return; } @@ -393,9 +382,6 @@ namespace StardewModdingAPI.Framework // This should *always* run, even when suppressing mod events, since the game uses // this too. For example, doing this after mod event suppression would prevent the // user from doing anything on the overnight shipping screen. -#if !SMAPI_3_0_STRICT - SInputState previousInputState = this.Input.Clone(); -#endif SInputState inputState = this.Input; if (this.IsActive) inputState.TrueUpdate(); @@ -416,9 +402,6 @@ namespace StardewModdingAPI.Framework this.IsBetweenCreateEvents = true; this.Monitor.Log("Context: before save creation.", LogLevel.Trace); events.SaveCreating.RaiseEmpty(); -#if !SMAPI_3_0_STRICT - events.Legacy_BeforeCreateSave.Raise(); -#endif } // raise before-save @@ -427,9 +410,6 @@ namespace StardewModdingAPI.Framework this.IsBetweenSaveEvents = true; this.Monitor.Log("Context: before save.", LogLevel.Trace); events.Saving.RaiseEmpty(); -#if !SMAPI_3_0_STRICT - events.Legacy_BeforeSave.Raise(); -#endif } // suppress non-save events @@ -437,9 +417,6 @@ namespace StardewModdingAPI.Framework SGame.TicksElapsed++; base.Update(gameTime); events.UnvalidatedUpdateTicked.RaiseEmpty(); -#if !SMAPI_3_0_STRICT - events.Legacy_UnvalidatedUpdateTick.Raise(); -#endif return; } if (this.IsBetweenCreateEvents) @@ -449,9 +426,6 @@ namespace StardewModdingAPI.Framework this.Monitor.Log($"Context: after save creation, starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); this.OnLoadStageChanged(LoadStage.CreatedSaveFile); events.SaveCreated.RaiseEmpty(); -#if !SMAPI_3_0_STRICT - events.Legacy_AfterCreateSave.Raise(); -#endif } if (this.IsBetweenSaveEvents) { @@ -460,10 +434,6 @@ namespace StardewModdingAPI.Framework this.Monitor.Log($"Context: after save, starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); events.Saved.RaiseEmpty(); events.DayStarted.RaiseEmpty(); -#if !SMAPI_3_0_STRICT - events.Legacy_AfterSave.Raise(); - events.Legacy_AfterDayStarted.Raise(); -#endif } /********* @@ -492,15 +462,8 @@ namespace StardewModdingAPI.Framework *********/ if (this.Watchers.LocaleWatcher.IsChanged) { - var was = this.Watchers.LocaleWatcher.PreviousValue; - var now = this.Watchers.LocaleWatcher.CurrentValue; - - this.Monitor.Log($"Context: locale set to {now}.", LogLevel.Trace); - + this.Monitor.Log($"Context: locale set to {this.Watchers.LocaleWatcher.CurrentValue}.", LogLevel.Trace); this.OnLocaleChanged(); -#if !SMAPI_3_0_STRICT - events.Legacy_LocaleChanged.Raise(new EventArgsValueChanged(was.ToString(), now.ToString())); -#endif this.Watchers.LocaleWatcher.Reset(); } @@ -527,10 +490,6 @@ namespace StardewModdingAPI.Framework this.OnLoadStageChanged(LoadStage.Ready); events.SaveLoaded.RaiseEmpty(); events.DayStarted.RaiseEmpty(); -#if !SMAPI_3_0_STRICT - events.Legacy_AfterLoad.Raise(); - events.Legacy_AfterDayStarted.Raise(); -#endif } /********* @@ -549,9 +508,6 @@ namespace StardewModdingAPI.Framework Point newSize = this.Watchers.WindowSizeWatcher.CurrentValue; events.WindowResized.Raise(new WindowResizedEventArgs(oldSize, newSize)); -#if !SMAPI_3_0_STRICT - events.Legacy_Resize.Raise(); -#endif this.Watchers.WindowSizeWatcher.Reset(); } @@ -610,23 +566,6 @@ namespace StardewModdingAPI.Framework this.Monitor.Log($"Events: button {button} pressed.", LogLevel.Trace); events.ButtonPressed.Raise(new ButtonPressedEventArgs(button, cursor, inputState)); - -#if !SMAPI_3_0_STRICT - // legacy events - events.Legacy_ButtonPressed.Raise(new EventArgsInput(button, cursor, inputState.SuppressButtons)); - if (button.TryGetKeyboard(out Keys key)) - { - if (key != Keys.None) - events.Legacy_KeyPressed.Raise(new EventArgsKeyPressed(key)); - } - else if (button.TryGetController(out Buttons controllerButton)) - { - if (controllerButton == Buttons.LeftTrigger || controllerButton == Buttons.RightTrigger) - events.Legacy_ControllerTriggerPressed.Raise(new EventArgsControllerTriggerPressed(PlayerIndex.One, controllerButton, controllerButton == Buttons.LeftTrigger ? inputState.RealController.Triggers.Left : inputState.RealController.Triggers.Right)); - else - events.Legacy_ControllerButtonPressed.Raise(new EventArgsControllerButtonPressed(PlayerIndex.One, controllerButton)); - } -#endif } else if (status == InputStatus.Released) { @@ -634,33 +573,8 @@ namespace StardewModdingAPI.Framework this.Monitor.Log($"Events: button {button} released.", LogLevel.Trace); events.ButtonReleased.Raise(new ButtonReleasedEventArgs(button, cursor, inputState)); - -#if !SMAPI_3_0_STRICT - // legacy events - events.Legacy_ButtonReleased.Raise(new EventArgsInput(button, cursor, inputState.SuppressButtons)); - if (button.TryGetKeyboard(out Keys key)) - { - if (key != Keys.None) - events.Legacy_KeyReleased.Raise(new EventArgsKeyPressed(key)); - } - else if (button.TryGetController(out Buttons controllerButton)) - { - if (controllerButton == Buttons.LeftTrigger || controllerButton == Buttons.RightTrigger) - events.Legacy_ControllerTriggerReleased.Raise(new EventArgsControllerTriggerReleased(PlayerIndex.One, controllerButton, controllerButton == Buttons.LeftTrigger ? inputState.RealController.Triggers.Left : inputState.RealController.Triggers.Right)); - else - events.Legacy_ControllerButtonReleased.Raise(new EventArgsControllerButtonReleased(PlayerIndex.One, controllerButton)); - } -#endif } } - -#if !SMAPI_3_0_STRICT - // raise legacy state-changed events - if (inputState.RealKeyboard != previousInputState.RealKeyboard) - events.Legacy_KeyboardChanged.Raise(new EventArgsKeyboardStateChanged(previousInputState.RealKeyboard, inputState.RealKeyboard)); - if (inputState.RealMouse != previousInputState.RealMouse) - events.Legacy_MouseChanged.Raise(new EventArgsMouseStateChanged(previousInputState.RealMouse, inputState.RealMouse, new Point((int)previousInputState.CursorPosition.ScreenPixels.X, (int)previousInputState.CursorPosition.ScreenPixels.Y), new Point((int)inputState.CursorPosition.ScreenPixels.X, (int)inputState.CursorPosition.ScreenPixels.Y))); -#endif } } @@ -678,12 +592,6 @@ namespace StardewModdingAPI.Framework // raise menu events events.MenuChanged.Raise(new MenuChangedEventArgs(was, now)); -#if !SMAPI_3_0_STRICT - if (now != null) - events.Legacy_MenuChanged.Raise(new EventArgsClickableMenuChanged(was, now)); - else - events.Legacy_MenuClosed.Raise(new EventArgsClickableMenuClosed(was)); -#endif } /********* @@ -711,9 +619,6 @@ namespace StardewModdingAPI.Framework } events.LocationListChanged.Raise(new LocationListChangedEventArgs(added, removed)); -#if !SMAPI_3_0_STRICT - events.Legacy_LocationsChanged.Raise(new EventArgsLocationsChanged(added, removed)); -#endif } // raise location contents changed @@ -730,9 +635,6 @@ namespace StardewModdingAPI.Framework watcher.BuildingsWatcher.Reset(); events.BuildingListChanged.Raise(new BuildingListChangedEventArgs(location, added, removed)); -#if !SMAPI_3_0_STRICT - events.Legacy_BuildingsChanged.Raise(new EventArgsLocationBuildingsChanged(location, added, removed)); -#endif } // debris changed @@ -777,9 +679,6 @@ namespace StardewModdingAPI.Framework watcher.ObjectsWatcher.Reset(); events.ObjectListChanged.Raise(new ObjectListChangedEventArgs(location, added, removed)); -#if !SMAPI_3_0_STRICT - events.Legacy_ObjectsChanged.Raise(new EventArgsLocationObjectsChanged(location, added, removed)); -#endif } // terrain features changed @@ -809,9 +708,6 @@ namespace StardewModdingAPI.Framework this.Monitor.Log($"Events: time changed from {was} to {now}.", LogLevel.Trace); events.TimeChanged.Raise(new TimeChangedEventArgs(was, now)); -#if !SMAPI_3_0_STRICT - events.Legacy_TimeOfDayChanged.Raise(new EventArgsIntChanged(was, now)); -#endif } else this.Watchers.TimeWatcher.Reset(); @@ -829,9 +725,6 @@ namespace StardewModdingAPI.Framework GameLocation oldLocation = playerTracker.LocationWatcher.PreviousValue; events.Warped.Raise(new WarpedEventArgs(playerTracker.Player, oldLocation, newLocation)); -#if !SMAPI_3_0_STRICT - events.Legacy_PlayerWarped.Raise(new EventArgsPlayerWarped(oldLocation, newLocation)); -#endif } // raise player leveled up a skill @@ -841,9 +734,6 @@ namespace StardewModdingAPI.Framework this.Monitor.Log($"Events: player skill '{pair.Key}' changed from {pair.Value.PreviousValue} to {pair.Value.CurrentValue}.", LogLevel.Trace); events.LevelChanged.Raise(new LevelChangedEventArgs(playerTracker.Player, pair.Key, pair.Value.PreviousValue, pair.Value.CurrentValue)); -#if !SMAPI_3_0_STRICT - events.Legacy_LeveledUp.Raise(new EventArgsLevelUp((EventArgsLevelUp.LevelType)pair.Key, pair.Value.CurrentValue)); -#endif } // raise player inventory changed @@ -853,9 +743,6 @@ namespace StardewModdingAPI.Framework if (this.Monitor.IsVerbose) this.Monitor.Log("Events: player inventory changed.", LogLevel.Trace); events.InventoryChanged.Raise(new InventoryChangedEventArgs(playerTracker.Player, changedItems)); -#if !SMAPI_3_0_STRICT - events.Legacy_InventoryChanged.Raise(new EventArgsInventoryChanged(Game1.player.Items, changedItems)); -#endif } // raise mine level changed @@ -863,9 +750,6 @@ namespace StardewModdingAPI.Framework { if (this.Monitor.IsVerbose) this.Monitor.Log($"Context: mine level changed to {mineLevel}.", LogLevel.Trace); -#if !SMAPI_3_0_STRICT - events.Legacy_MineLevelChanged.Raise(new EventArgsMineLevelChanged(playerTracker.MineLevelWatcher.PreviousValue, mineLevel)); -#endif } } this.Watchers.CurrentPlayerTracker?.Reset(); @@ -910,25 +794,6 @@ namespace StardewModdingAPI.Framework /********* ** Update events *********/ -#if !SMAPI_3_0_STRICT - events.Legacy_UnvalidatedUpdateTick.Raise(); - if (isFirstTick) - events.Legacy_FirstUpdateTick.Raise(); - events.Legacy_UpdateTick.Raise(); - if (SGame.TicksElapsed % 2 == 0) - events.Legacy_SecondUpdateTick.Raise(); - if (SGame.TicksElapsed % 4 == 0) - events.Legacy_FourthUpdateTick.Raise(); - if (SGame.TicksElapsed % 8 == 0) - events.Legacy_EighthUpdateTick.Raise(); - if (SGame.TicksElapsed % 15 == 0) - events.Legacy_QuarterSecondTick.Raise(); - if (SGame.TicksElapsed % 30 == 0) - events.Legacy_HalfSecondTick.Raise(); - if (SGame.TicksElapsed % 60 == 0) - events.Legacy_OneSecondTick.Raise(); -#endif - this.UpdateCrashTimer.Reset(); } catch (Exception ex) @@ -1014,14 +879,8 @@ namespace StardewModdingAPI.Framework try { events.RenderingActiveMenu.RaiseEmpty(); -#if !SMAPI_3_0_STRICT - events.Legacy_OnPreRenderGuiEvent.Raise(); -#endif activeClickableMenu.draw(Game1.spriteBatch); events.RenderedActiveMenu.RaiseEmpty(); -#if !SMAPI_3_0_STRICT - events.Legacy_OnPostRenderGuiEvent.Raise(); -#endif } catch (Exception ex) { @@ -1029,10 +888,6 @@ namespace StardewModdingAPI.Framework activeClickableMenu.exitThisMenu(); } events.Rendered.RaiseEmpty(); -#if !SMAPI_3_0_STRICT - events.Legacy_OnPostRenderEvent.Raise(); -#endif - Game1.spriteBatch.End(); } if (Game1.overlayMenu != null) @@ -1055,14 +910,8 @@ namespace StardewModdingAPI.Framework { Game1.activeClickableMenu.drawBackground(Game1.spriteBatch); events.RenderingActiveMenu.RaiseEmpty(); -#if !SMAPI_3_0_STRICT - events.Legacy_OnPreRenderGuiEvent.Raise(); -#endif Game1.activeClickableMenu.draw(Game1.spriteBatch); events.RenderedActiveMenu.RaiseEmpty(); -#if !SMAPI_3_0_STRICT - events.Legacy_OnPostRenderGuiEvent.Raise(); -#endif } catch (Exception ex) { @@ -1070,9 +919,6 @@ namespace StardewModdingAPI.Framework Game1.activeClickableMenu.exitThisMenu(); } events.Rendered.RaiseEmpty(); -#if !SMAPI_3_0_STRICT - events.Legacy_OnPostRenderEvent.Raise(); -#endif Game1.spriteBatch.End(); this.drawOverlays(Game1.spriteBatch); if ((double)Game1.options.zoomLevel != 1.0) @@ -1097,9 +943,6 @@ namespace StardewModdingAPI.Framework Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3686"), new Vector2(16f, 32f), new Color(0, (int)byte.MaxValue, 0)); Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.parseText(Game1.errorMessage, Game1.dialogueFont, Game1.graphics.GraphicsDevice.Viewport.Width), new Vector2(16f, 48f), Color.White); events.Rendered.RaiseEmpty(); -#if !SMAPI_3_0_STRICT - events.Legacy_OnPostRenderEvent.Raise(); -#endif Game1.spriteBatch.End(); } else if (Game1.currentMinigame != null) @@ -1112,9 +955,6 @@ namespace StardewModdingAPI.Framework Game1.spriteBatch.End(); } this.drawOverlays(Game1.spriteBatch); -#if !SMAPI_3_0_STRICT - this.RaisePostRender(needsNewBatch: true); -#endif if ((double)Game1.options.zoomLevel == 1.0) return; this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); @@ -1132,14 +972,8 @@ namespace StardewModdingAPI.Framework try { events.RenderingActiveMenu.RaiseEmpty(); -#if !SMAPI_3_0_STRICT - events.Legacy_OnPreRenderGuiEvent.Raise(); -#endif Game1.activeClickableMenu.draw(Game1.spriteBatch); events.RenderedActiveMenu.RaiseEmpty(); -#if !SMAPI_3_0_STRICT - events.Legacy_OnPostRenderGuiEvent.Raise(); -#endif } catch (Exception ex) { @@ -1229,9 +1063,6 @@ namespace StardewModdingAPI.Framework if (++batchOpens == 1) events.Rendering.RaiseEmpty(); events.RenderingWorld.RaiseEmpty(); -#if !SMAPI_3_0_STRICT - events.Legacy_OnPreRenderEvent.Raise(); -#endif if (Game1.background != null) Game1.background.draw(Game1.spriteBatch); Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); @@ -1389,7 +1220,7 @@ namespace StardewModdingAPI.Framework } Game1.drawPlayerHeldObject(Game1.player); } - label_129: + label_129: if ((Game1.player.UsingTool || Game1.pickingTool) && Game1.player.CurrentTool != null && ((!Game1.player.CurrentTool.Name.Equals("Seeds") || Game1.pickingTool) && (Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), (int)Game1.player.Position.Y - 38), Game1.viewport.Size) != null && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), Game1.player.getStandingY()), Game1.viewport.Size) == null))) Game1.drawTool(Game1.player); if (Game1.currentLocation.Map.GetLayer("AlwaysFront") != null) @@ -1544,14 +1375,8 @@ namespace StardewModdingAPI.Framework if ((Game1.displayHUD || Game1.eventUp) && (Game1.currentBillboard == 0 && Game1.gameMode == (byte)3) && (!Game1.freezeControls && !Game1.panMode && !Game1.HostPaused)) { events.RenderingHud.RaiseEmpty(); -#if !SMAPI_3_0_STRICT - events.Legacy_OnPreRenderHudEvent.Raise(); -#endif this.drawHUD(); events.RenderedHud.RaiseEmpty(); -#if !SMAPI_3_0_STRICT - events.Legacy_OnPostRenderHudEvent.Raise(); -#endif } else if (Game1.activeClickableMenu == null && Game1.farmEvent == null) Game1.spriteBatch.Draw(Game1.mouseCursors, new Vector2((float)Game1.getOldMouseX(), (float)Game1.getOldMouseY()), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.mouseCursors, 0, 16, 16)), Color.White, 0.0f, Vector2.Zero, (float)(4.0 + (double)Game1.dialogueButtonScale / 150.0), SpriteEffects.None, 1f); @@ -1660,14 +1485,8 @@ namespace StardewModdingAPI.Framework try { events.RenderingActiveMenu.RaiseEmpty(); -#if !SMAPI_3_0_STRICT - events.Legacy_OnPreRenderGuiEvent.Raise(); -#endif Game1.activeClickableMenu.draw(Game1.spriteBatch); events.RenderedActiveMenu.RaiseEmpty(); -#if !SMAPI_3_0_STRICT - events.Legacy_OnPostRenderGuiEvent.Raise(); -#endif } catch (Exception ex) { @@ -1684,9 +1503,6 @@ namespace StardewModdingAPI.Framework } events.Rendered.RaiseEmpty(); -#if !SMAPI_3_0_STRICT - events.Legacy_OnPostRenderEvent.Raise(); -#endif Game1.spriteBatch.End(); this.drawOverlays(Game1.spriteBatch); this.renderScreenBuffer(); @@ -1694,24 +1510,5 @@ namespace StardewModdingAPI.Framework } } } - - /**** - ** Methods - ****/ -#if !SMAPI_3_0_STRICT - /// Raise the if there are any listeners. - /// Whether to create a new sprite batch. - private void RaisePostRender(bool needsNewBatch = false) - { - if (this.Events.Legacy_OnPostRenderEvent.HasListeners()) - { - if (needsNewBatch) - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); - this.Events.Legacy_OnPostRenderEvent.Raise(); - if (needsNewBatch) - Game1.spriteBatch.End(); - } - } -#endif } } diff --git a/src/SMAPI/Framework/SMultiplayer.cs b/src/SMAPI/Framework/SMultiplayer.cs index dde71092..ffe2320b 100644 --- a/src/SMAPI/Framework/SMultiplayer.cs +++ b/src/SMAPI/Framework/SMultiplayer.cs @@ -89,24 +89,6 @@ namespace StardewModdingAPI.Framework this.HostPeer = null; } -#if !SMAPI_3_0_STRICT - /// Handle sync messages from other players and perform other initial sync logic. - public override void UpdateEarly() - { - this.EventManager.Legacy_BeforeMainSync.Raise(); - base.UpdateEarly(); - this.EventManager.Legacy_AfterMainSync.Raise(); - } - - /// Broadcast sync messages to other players and perform other final sync logic. - public override void UpdateLate(bool forceSync = false) - { - this.EventManager.Legacy_BeforeMainBroadcast.Raise(); - base.UpdateLate(forceSync); - this.EventManager.Legacy_AfterMainBroadcast.Raise(); - } -#endif - /// Initialise a client before the game connects to a remote server. /// The client to initialise. public override Client InitClient(Client client) diff --git a/src/SMAPI/IAssetDataForDictionary.cs b/src/SMAPI/IAssetDataForDictionary.cs index 911599d9..1136316f 100644 --- a/src/SMAPI/IAssetDataForDictionary.cs +++ b/src/SMAPI/IAssetDataForDictionary.cs @@ -1,32 +1,7 @@ -using System; using System.Collections.Generic; -using StardewModdingAPI.Framework.Content; namespace StardewModdingAPI { /// Encapsulates access and changes to dictionary content being read from a data file. - public interface IAssetDataForDictionary : IAssetData> - { -#if !SMAPI_3_0_STRICT - /********* - ** Public methods - *********/ - /// Add or replace an entry in the dictionary. - /// The entry key. - /// The entry value. - [Obsolete("Access " + nameof(AssetData>.Data) + "field directly.")] - void Set(TKey key, TValue value); - - /// Add or replace an entry in the dictionary. - /// The entry key. - /// A callback which accepts the current value and returns the new value. - [Obsolete("Access " + nameof(AssetData>.Data) + "field directly.")] - void Set(TKey key, Func value); - - /// Dynamically replace values in the dictionary. - /// A lambda which takes the current key and value for an entry, and returns the new value. - [Obsolete("Access " + nameof(AssetData>.Data) + "field directly.")] - void Set(Func replacer); -#endif - } + public interface IAssetDataForDictionary : IAssetData> { } } diff --git a/src/SMAPI/IModHelper.cs b/src/SMAPI/IModHelper.cs index 0220b4f7..cd746e06 100644 --- a/src/SMAPI/IModHelper.cs +++ b/src/SMAPI/IModHelper.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using StardewModdingAPI.Events; namespace StardewModdingAPI @@ -58,41 +56,5 @@ namespace StardewModdingAPI /// The config class type. /// The config settings to save. void WriteConfig(TConfig config) where TConfig : class, new(); - -#if !SMAPI_3_0_STRICT - /**** - ** Generic JSON files - ****/ - /// Read a JSON file. - /// The model type. - /// The file path relative to the mod directory. - /// Returns the deserialised model, or null if the file doesn't exist or is empty. - [Obsolete("Use " + nameof(IModHelper.Data) + "." + nameof(IDataHelper.ReadJsonFile) + " instead")] - TModel ReadJsonFile(string path) where TModel : class; - - /// Save to a JSON file. - /// The model type. - /// The file path relative to the mod directory. - /// The model to save. - [Obsolete("Use " + nameof(IModHelper.Data) + "." + nameof(IDataHelper.WriteJsonFile) + " instead")] - void WriteJsonFile(string path, TModel model) where TModel : class; - - /**** - ** Content packs - ****/ - /// Manually create a transitional content pack to support pre-SMAPI content packs. This provides a way to access legacy content packs using the SMAPI content pack APIs, but the content pack will not be visible in the log or validated by SMAPI. - /// The absolute directory path containing the content pack files. - /// The content pack's unique ID. - /// The content pack name. - /// The content pack description. - /// The content pack author's name. - /// The content pack version. - [Obsolete("Use " + nameof(IModHelper) + "." + nameof(IModHelper.ContentPacks) + "." + nameof(IContentPackHelper.CreateTemporary) + " instead")] - IContentPack CreateTransitionalContentPack(string directoryPath, string id, string name, string description, string author, ISemanticVersion version); - - /// Get all content packs loaded for this mod. - [Obsolete("Use " + nameof(IModHelper) + "." + nameof(IModHelper.ContentPacks) + "." + nameof(IContentPackHelper.GetOwned) + " instead")] - IEnumerable GetContentPacks(); -#endif } } diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index 272ceb09..72410d41 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -53,9 +53,6 @@ namespace StardewModdingAPI.Metadata yield return new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.locationSerializer), InstructionHandleResult.DetectedSaveSerialiser); yield return new EventFinder(typeof(ISpecialisedEvents).FullName, nameof(ISpecialisedEvents.UnvalidatedUpdateTicked), InstructionHandleResult.DetectedUnvalidatedUpdateTick); yield return new EventFinder(typeof(ISpecialisedEvents).FullName, nameof(ISpecialisedEvents.UnvalidatedUpdateTicking), InstructionHandleResult.DetectedUnvalidatedUpdateTick); -#if !SMAPI_3_0_STRICT - yield return new EventFinder(typeof(SpecialisedEvents).FullName, nameof(SpecialisedEvents.UnvalidatedUpdateTick), InstructionHandleResult.DetectedUnvalidatedUpdateTick); -#endif /**** ** detect paranoid issues diff --git a/src/SMAPI/SemanticVersion.cs b/src/SMAPI/SemanticVersion.cs index ec2d9e40..acdc92f8 100644 --- a/src/SMAPI/SemanticVersion.cs +++ b/src/SMAPI/SemanticVersion.cs @@ -1,6 +1,5 @@ using System; using Newtonsoft.Json; -using StardewModdingAPI.Framework; namespace StardewModdingAPI { @@ -26,19 +25,6 @@ namespace StardewModdingAPI /// The patch version for backwards-compatible bug fixes. public int PatchVersion => this.Version.PatchVersion; -#if !SMAPI_3_0_STRICT - /// An optional build tag. - [Obsolete("Use " + nameof(ISemanticVersion.PrereleaseTag) + " instead")] - public string Build - { - get - { - SCore.DeprecationManager?.Warn($"{nameof(ISemanticVersion)}.{nameof(ISemanticVersion.Build)}", "2.8", DeprecationLevel.PendingRemoval); - return this.Version.PrereleaseTag; - } - } -#endif - /// An optional prerelease tag. public string PrereleaseTag => this.Version.PrereleaseTag; -- cgit From dc0556ff5feead4ced16b82f407b6b271cbb3d30 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 7 Mar 2019 18:19:11 -0500 Subject: fix log level for multiplayer 'received message' logs --- docs/release-notes.md | 1 + src/SMAPI/Framework/SMultiplayer.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 2f01ee93..c10e663d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -5,6 +5,7 @@ These changes have not been released yet. * For players: * Fixed Save Backup not pruning old backups if they're uncompressed. * Fixed issues when a farmhand reconnects before the game notices they're disconnected. + * Fixed 'received message' logs shown in non-developer mode. * For modders: * Added `IContentPack.HasFile` method. diff --git a/src/SMAPI/Framework/SMultiplayer.cs b/src/SMAPI/Framework/SMultiplayer.cs index ffe2320b..382910a0 100644 --- a/src/SMAPI/Framework/SMultiplayer.cs +++ b/src/SMAPI/Framework/SMultiplayer.cs @@ -433,7 +433,7 @@ namespace StardewModdingAPI.Framework ModMessageModel model = this.JsonHelper.Deserialise(json); HashSet playerIDs = new HashSet(model.ToPlayerIDs ?? this.GetKnownPlayerIDs()); if (this.Monitor.IsVerbose) - this.Monitor.Log($"Received message: {json}."); + this.Monitor.Log($"Received message: {json}.", LogLevel.Trace); // notify local mods if (playerIDs.Contains(Game1.player.UniqueMultiplayerID)) -- cgit From 332bcfa5a19509352aa417a04a677b5701c16986 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 21 Mar 2019 23:12:26 -0400 Subject: add content pack translations --- docs/release-notes.md | 1 + src/SMAPI/Framework/ContentPack.cs | 7 +++++- src/SMAPI/Framework/IModMetadata.cs | 10 ++++++-- src/SMAPI/Framework/ModLoading/ModMetadata.cs | 12 ++++++++-- src/SMAPI/Framework/SCore.cs | 33 +++++++++++++-------------- src/SMAPI/IContentPack.cs | 3 +++ 6 files changed, 44 insertions(+), 22 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index c10e663d..afcf0066 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ These changes have not been released yet. * Fixed 'received message' logs shown in non-developer mode. * For modders: + * Added support for content pack translations. * Added `IContentPack.HasFile` method. * Dropped support for all deprecated APIs. * Updated to Json.NET 12.0.1. diff --git a/src/SMAPI/Framework/ContentPack.cs b/src/SMAPI/Framework/ContentPack.cs index 5384d98f..829a7dc1 100644 --- a/src/SMAPI/Framework/ContentPack.cs +++ b/src/SMAPI/Framework/ContentPack.cs @@ -30,6 +30,9 @@ namespace StardewModdingAPI.Framework /// The content pack's manifest. public IManifest Manifest { get; } + /// Provides translations stored in the content pack's i18n folder. See for more info. + public ITranslationHelper Translation { get; } + /********* ** Public methods @@ -38,12 +41,14 @@ namespace StardewModdingAPI.Framework /// The full path to the content pack's folder. /// The content pack's manifest. /// Provides an API for loading content assets. + /// Provides translations stored in the content pack's i18n folder. /// Encapsulates SMAPI's JSON file parsing. - public ContentPack(string directoryPath, IManifest manifest, IContentHelper content, JsonHelper jsonHelper) + public ContentPack(string directoryPath, IManifest manifest, IContentHelper content, ITranslationHelper translation, JsonHelper jsonHelper) { this.DirectoryPath = directoryPath; this.Manifest = manifest; this.Content = content; + this.Translation = translation; this.JsonHelper = jsonHelper; } diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs index 38514959..32870c2a 100644 --- a/src/SMAPI/Framework/IModMetadata.cs +++ b/src/SMAPI/Framework/IModMetadata.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using StardewModdingAPI.Framework.ModHelpers; using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; using StardewModdingAPI.Toolkit.Framework.ModData; @@ -42,6 +43,9 @@ namespace StardewModdingAPI.Framework /// The content pack instance (if loaded and is true). IContentPack ContentPack { get; } + /// The translations for this mod (if loaded). + TranslationHelper Translations { get; } + /// Writes messages to the console and log file as this mod. IMonitor Monitor { get; } @@ -67,12 +71,14 @@ namespace StardewModdingAPI.Framework /// Set the mod instance. /// The mod instance to set. - IModMetadata SetMod(IMod mod); + /// The translations for this mod (if loaded). + IModMetadata SetMod(IMod mod, TranslationHelper translations); /// Set the mod instance. /// The contentPack instance to set. /// Writes messages to the console and log file. - IModMetadata SetMod(IContentPack contentPack, IMonitor monitor); + /// The translations for this mod (if loaded). + IModMetadata SetMod(IContentPack contentPack, IMonitor monitor, TranslationHelper translations); /// Set the mod-provided API instance. /// The mod-provided API. diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index 4ff021b7..39f2f482 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using StardewModdingAPI.Framework.ModHelpers; using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; using StardewModdingAPI.Toolkit.Framework.ModData; using StardewModdingAPI.Toolkit.Framework.UpdateData; @@ -46,6 +47,9 @@ namespace StardewModdingAPI.Framework.ModLoading /// The content pack instance (if loaded and is true). public IContentPack ContentPack { get; private set; } + /// The translations for this mod (if loaded). + public TranslationHelper Translations { get; private set; } + /// Writes messages to the console and log file as this mod. public IMonitor Monitor { get; private set; } @@ -100,26 +104,30 @@ namespace StardewModdingAPI.Framework.ModLoading /// Set the mod instance. /// The mod instance to set. - public IModMetadata SetMod(IMod mod) + /// The translations for this mod (if loaded). + public IModMetadata SetMod(IMod mod, TranslationHelper translations) { if (this.ContentPack != null) throw new InvalidOperationException("A mod can't be both an assembly mod and content pack."); this.Mod = mod; this.Monitor = mod.Monitor; + this.Translations = translations; return this; } /// Set the mod instance. /// The contentPack instance to set. /// Writes messages to the console and log file. - public IModMetadata SetMod(IContentPack contentPack, IMonitor monitor) + /// The translations for this mod (if loaded). + public IModMetadata SetMod(IContentPack contentPack, IMonitor monitor, TranslationHelper translations) { if (this.Mod != null) throw new InvalidOperationException("A mod can't be both an assembly mod and content pack."); this.ContentPack = contentPack; this.Monitor = monitor; + this.Translations = translations; return this; } diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 06a2e0af..2f0e0f05 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -427,8 +427,8 @@ namespace StardewModdingAPI.Framework LocalizedContentManager.LanguageCode languageCode = this.ContentCore.Language; // update mod translation helpers - foreach (IModMetadata mod in this.ModRegistry.GetAll(contentPacks: false)) - (mod.Mod.Helper.Translation as TranslationHelper)?.SetLocale(locale, languageCode); + foreach (IModMetadata mod in this.ModRegistry.GetAll()) + mod.Translations.SetLocale(locale, languageCode); } /// Run a loop handling console input. @@ -725,8 +725,9 @@ namespace StardewModdingAPI.Framework LogSkip(contentPack, errorPhrase, errorDetails); } } - IModMetadata[] loadedContentPacks = this.ModRegistry.GetAll(assemblyMods: false).ToArray(); - IModMetadata[] loadedMods = this.ModRegistry.GetAll(contentPacks: false).ToArray(); + IModMetadata[] loaded = this.ModRegistry.GetAll().ToArray(); + IModMetadata[] loadedContentPacks = loaded.Where(p => p.IsContentPack).ToArray(); + IModMetadata[] loadedMods = loaded.Where(p => !p.IsContentPack).ToArray(); // unlock content packs this.ModRegistry.AreAllModsLoaded = true; @@ -766,10 +767,10 @@ namespace StardewModdingAPI.Framework } // log mod warnings - this.LogModWarnings(this.ModRegistry.GetAll().ToArray(), skippedMods); + this.LogModWarnings(loaded, skippedMods); // initialise translations - this.ReloadTranslations(loadedMods); + this.ReloadTranslations(loaded); // initialise loaded non-content-pack mods foreach (IModMetadata metadata in loadedMods) @@ -919,8 +920,9 @@ namespace StardewModdingAPI.Framework IManifest manifest = mod.Manifest; IMonitor monitor = this.GetSecondaryMonitor(mod.DisplayName); IContentHelper contentHelper = new ContentHelper(this.ContentCore, mod.DirectoryPath, manifest.UniqueID, mod.DisplayName, monitor); - IContentPack contentPack = new ContentPack(mod.DirectoryPath, manifest, contentHelper, jsonHelper); - mod.SetMod(contentPack, monitor); + TranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentCore.GetLocale(), contentCore.Language); + IContentPack contentPack = new ContentPack(mod.DirectoryPath, manifest, contentHelper, translationHelper, jsonHelper); + mod.SetMod(contentPack, monitor, translationHelper); this.ModRegistry.Add(mod); errorReasonPhrase = null; @@ -982,6 +984,7 @@ namespace StardewModdingAPI.Framework // init mod helpers IMonitor monitor = this.GetSecondaryMonitor(mod.DisplayName); + TranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentCore.GetLocale(), contentCore.Language); IModHelper modHelper; { IModEvents events = new ModEvents(mod, this.EventManager); @@ -992,13 +995,13 @@ namespace StardewModdingAPI.Framework IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, mod.DisplayName, this.Reflection); IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry, proxyFactory, monitor); IMultiplayerHelper multiplayerHelper = new MultiplayerHelper(manifest.UniqueID, this.GameInstance.Multiplayer); - ITranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentCore.GetLocale(), contentCore.Language); IContentPack CreateFakeContentPack(string packDirPath, IManifest packManifest) { IMonitor packMonitor = this.GetSecondaryMonitor(packManifest.Name); IContentHelper packContentHelper = new ContentHelper(contentCore, packDirPath, packManifest.UniqueID, packManifest.Name, packMonitor); - return new ContentPack(packDirPath, packManifest, packContentHelper, this.Toolkit.JsonHelper); + ITranslationHelper packTranslationHelper = new TranslationHelper(packManifest.UniqueID, packManifest.Name, contentCore.GetLocale(), contentCore.Language); + return new ContentPack(packDirPath, packManifest, packContentHelper, packTranslationHelper, this.Toolkit.JsonHelper); } modHelper = new ModHelper(manifest.UniqueID, mod.DirectoryPath, this.GameInstance.Input, events, contentHelper, contentPackHelper, commandHelper, dataHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper); @@ -1010,7 +1013,7 @@ namespace StardewModdingAPI.Framework modEntry.Monitor = monitor; // track mod - mod.SetMod(modEntry); + mod.SetMod(modEntry, translationHelper); this.ModRegistry.Add(mod); return true; } @@ -1025,7 +1028,7 @@ namespace StardewModdingAPI.Framework /// Write a summary of mod warnings to the console and log. /// The loaded mods. /// The mods which were skipped, along with the friendly and developer reasons. - private void LogModWarnings(IModMetadata[] mods, IDictionary> skippedMods) + private void LogModWarnings(IEnumerable mods, IDictionary> skippedMods) { // get mods with warnings IModMetadata[] modsWithWarnings = mods.Where(p => p.Warnings != ModWarning.None).ToArray(); @@ -1165,9 +1168,6 @@ namespace StardewModdingAPI.Framework JsonHelper jsonHelper = this.Toolkit.JsonHelper; foreach (IModMetadata metadata in mods) { - if (metadata.IsContentPack) - throw new InvalidOperationException("Can't reload translations for a content pack."); - // read translation files IDictionary> translations = new Dictionary>(); DirectoryInfo translationsDir = new DirectoryInfo(Path.Combine(metadata.DirectoryPath, "i18n")); @@ -1217,8 +1217,7 @@ namespace StardewModdingAPI.Framework } // update translation - TranslationHelper translationHelper = (TranslationHelper)metadata.Mod.Helper.Translation; - translationHelper.SetTranslations(translations); + metadata.Translations.SetTranslations(translations); } } diff --git a/src/SMAPI/IContentPack.cs b/src/SMAPI/IContentPack.cs index 32cbc6fc..7085c538 100644 --- a/src/SMAPI/IContentPack.cs +++ b/src/SMAPI/IContentPack.cs @@ -17,6 +17,9 @@ namespace StardewModdingAPI /// The content pack's manifest. IManifest Manifest { get; } + /// Provides translations stored in the content pack's i18n folder. See for more info. + ITranslationHelper Translation { get; } + /********* ** Public methods -- cgit From f78502a3a4a133b93cf9bf01eb426867e7798045 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 21 Mar 2019 23:14:31 -0400 Subject: fix incorrect input check, update release notes --- docs/release-notes.md | 1 + src/SMAPI/Framework/Input/SInputState.cs | 3 +++ 2 files changed, 4 insertions(+) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index afcf0066..4fb7b6c3 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -3,6 +3,7 @@ These changes have not been released yet. * For players: + * Improved performance. * Fixed Save Backup not pruning old backups if they're uncompressed. * Fixed issues when a farmhand reconnects before the game notices they're disconnected. * Fixed 'received message' logs shown in non-developer mode. diff --git a/src/SMAPI/Framework/Input/SInputState.cs b/src/SMAPI/Framework/Input/SInputState.cs index 96a7003a..a15272d5 100644 --- a/src/SMAPI/Framework/Input/SInputState.cs +++ b/src/SMAPI/Framework/Input/SInputState.cs @@ -94,7 +94,10 @@ namespace StardewModdingAPI.Framework.Input this.RealKeyboard = realKeyboard; this.RealMouse = realMouse; if (cursorAbsolutePos != this.CursorPositionImpl?.AbsolutePixels || playerTilePos != this.LastPlayerTile) + { + this.LastPlayerTile = playerTilePos; this.CursorPositionImpl = this.GetCursorPosition(realMouse, cursorAbsolutePos); + } // update suppressed states this.SuppressButtons.RemoveWhere(p => !this.GetStatus(activeButtons, p).IsDown()); -- cgit From 34633cfed9efb8a53b148584c1b3909bac69372f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 29 Mar 2019 23:58:06 -0400 Subject: bundle assets folder into mods by default --- docs/mod-build-config.md | 29 ++++++++++------------ .../Framework/ModFileManager.cs | 28 +++++++++++++++++---- src/SMAPI.ModBuildConfig/package.nuspec | 1 + 3 files changed, 37 insertions(+), 21 deletions(-) (limited to 'src') diff --git a/docs/mod-build-config.md b/docs/mod-build-config.md index 329f5faf..14d167f2 100644 --- a/docs/mod-build-config.md +++ b/docs/mod-build-config.md @@ -6,6 +6,7 @@ The package... * detects your game install path; * adds the assembly references you need (with automatic support for Linux/Mac/Windows); * packages the mod into your `Mods` folder when you rebuild the code (configurable); +* creates a release zip (configurable); * configures Visual Studio to enable debugging into the code when the game is running (_Windows only_); * adds C# analyzers to warn for Stardew Valley-specific issues. @@ -17,25 +18,21 @@ The package... * [Release notes](#release-notes) ## Install -**When creating a new mod:** - -1. Create an empty library project. +1. Create an empty library project.
(For an existing project, remove references to `Microsoft.Xna.*`, `MonoGame`, Stardew Valley, + `StardewModdingAPI`, and `xTile` instead.) 2. Reference the [`Pathoschild.Stardew.ModBuildConfig` NuGet package](https://www.nuget.org/packages/Pathoschild.Stardew.ModBuildConfig). 3. [Write your code](https://stardewvalleywiki.com/Modding:Creating_a_SMAPI_mod). 4. Compile on any platform. -**When migrating an existing mod:** - -1. Remove any project references to `Microsoft.Xna.*`, `MonoGame`, Stardew Valley, - `StardewModdingAPI`, and `xTile`. -2. Reference the [`Pathoschild.Stardew.ModBuildConfig` NuGet package](https://www.nuget.org/packages/Pathoschild.Stardew.ModBuildConfig). -3. Compile on any platform. - ## Configure ### Deploy files into the `Mods` folder -By default, your mod will be copied into the game's `Mods` folder (with a subfolder matching your -project name) when you rebuild the code. The package will automatically include your -`manifest.json`, any `i18n` files, and the build output. +Your mod is copied into the game's `Mods` folder (with a subfolder matching your project name) +when you rebuild the code. The package automatically includes... + +* any build output; +* your `manifest.json`; +* any [`i18n` files](https://stardewvalleywiki.com/Modding:Translations); +* the `assets` folder if present. To add custom files to the mod folder, just [add them to the build output](https://stackoverflow.com/a/10828462/262123). (If your project references another mod, make sure the reference is [_not_ marked 'copy local'](https://msdn.microsoft.com/en-us/library/t1zz5y8c(v=vs.100).aspx).) @@ -52,9 +49,9 @@ If you don't want to deploy the mod automatically, you can add this: ``` ### Create release zip -By default, a zip file will be created in the build output when you rebuild the code. This zip file -contains all the files needed to share your mod in the recommended format for uploading to Nexus -Mods or other sites. +A zip file is also created in the build output folder when you rebuild the code. This contains the +same files deployed to the `Mods` folder, in the recommended format for uploading to Nexus Mods or +other sites. You can change the zipped folder name (and zip name) by adding this above the first `` in your `.csproj`: diff --git a/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs b/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs index 4af3100f..6c11b0fe 100644 --- a/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs +++ b/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Text.RegularExpressions; using StardewModdingAPI.Toolkit.Serialisation; using StardewModdingAPI.Toolkit.Serialisation.Models; +using StardewModdingAPI.Toolkit.Utilities; namespace StardewModdingAPI.ModBuildConfig.Framework { @@ -61,18 +62,33 @@ namespace StardewModdingAPI.ModBuildConfig.Framework hasProjectTranslations = true; } + // project assets folder + bool hasAssetsFolder = false; + DirectoryInfo assetsFolder = new DirectoryInfo(Path.Combine(projectDir, "assets")); + if (assetsFolder.Exists) + { + foreach (FileInfo file in assetsFolder.EnumerateFiles("*", SearchOption.AllDirectories)) + { + string relativePath = PathUtilities.GetRelativePath(projectDir, file.FullName); + this.Files[relativePath] = file; + } + hasAssetsFolder = true; + } + // build output DirectoryInfo buildFolder = new DirectoryInfo(targetDir); foreach (FileInfo file in buildFolder.EnumerateFiles("*", SearchOption.AllDirectories)) { - // get relative paths - string relativePath = file.FullName.Replace(buildFolder.FullName, ""); - string relativeDirPath = file.Directory.FullName.Replace(buildFolder.FullName, ""); + // get path info + string relativePath = PathUtilities.GetRelativePath(buildFolder.FullName, file.FullName); + string[] segments = PathUtilities.GetSegments(relativePath); - // prefer project manifest/i18n files + // prefer project manifest/i18n/assets files if (hasProjectManifest && this.EqualsInvariant(relativePath, this.ManifestFileName)) continue; - if (hasProjectTranslations && this.EqualsInvariant(relativeDirPath, "i18n")) + if (hasProjectTranslations && this.EqualsInvariant(segments[0], "i18n")) + continue; + if (hasAssetsFolder && this.EqualsInvariant(segments[0], "assets")) continue; // handle ignored files @@ -149,6 +165,8 @@ namespace StardewModdingAPI.ModBuildConfig.Framework /// The string to compare with. private bool EqualsInvariant(string str, string other) { + if (str == null) + return other == null; return str.Equals(other, StringComparison.InvariantCultureIgnoreCase); } } diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec index d054fd1a..b308413f 100644 --- a/src/SMAPI.ModBuildConfig/package.nuspec +++ b/src/SMAPI.ModBuildConfig/package.nuspec @@ -13,6 +13,7 @@ Automates the build configuration for crossplatform Stardew Valley SMAPI mods. For Stardew Valley 1.3 or later. 2.2.1: + - If the project contains an `assets` folder, its contents are now included in the mod automatically. - Dropped support for very old versions of SMAPI and Visual Studio. - Fixed `Newtonsoft.Json.pdb` included in release zips when Json.NET is referenced directly. -- cgit From 4689eeb6a3af1aead00347fc2575bfebdee50113 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 30 Mar 2019 01:25:12 -0400 Subject: load mods much earlier so they can intercept all content assets --- docs/release-notes.md | 2 + src/SMAPI/Context.cs | 3 ++ src/SMAPI/Framework/ContentCoordinator.cs | 11 ++-- .../ContentManagers/GameContentManager.cs | 17 +++++- src/SMAPI/Framework/SCore.cs | 62 ++++++++++++++-------- src/SMAPI/Framework/SGame.cs | 9 +++- src/SMAPI/Framework/SGameConstructorHack.cs | 8 ++- 7 files changed, 85 insertions(+), 27 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 4fb7b6c3..649cd774 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -11,6 +11,8 @@ These changes have not been released yet. * For modders: * Added support for content pack translations. * Added `IContentPack.HasFile` method. + * Added `Context.IsGameLaunched` field. + * Mods are now loaded much earlier in the game launch. This lets mods intercept any content asset, but the game is not fully initialised when `Entry` is called (use the `GameLaunched` event if you need to run code when the game is initialised). * Dropped support for all deprecated APIs. * Updated to Json.NET 12.0.1. diff --git a/src/SMAPI/Context.cs b/src/SMAPI/Context.cs index 1cdef7f1..a933752d 100644 --- a/src/SMAPI/Context.cs +++ b/src/SMAPI/Context.cs @@ -14,6 +14,9 @@ namespace StardewModdingAPI /**** ** Public ****/ + /// Whether the game has performed core initialisation. This becomes true right before the first update tick.. + public static bool IsGameLaunched { get; internal set; } + /// Whether the player has loaded a save and the world has finished initialising. public static bool IsWorldReady { get; internal set; } diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index ee654081..caeb7ee9 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -36,6 +36,9 @@ namespace StardewModdingAPI.Framework /// Encapsulates SMAPI's JSON file parsing. private readonly JsonHelper JsonHelper; + /// A callback to invoke the first time *any* game content manager loads an asset. + private readonly Action OnLoadingFirstAsset; + /// The loaded content managers (including the ). private readonly IList ContentManagers = new List(); @@ -72,14 +75,16 @@ namespace StardewModdingAPI.Framework /// Encapsulates monitoring and logging. /// Simplifies access to private code. /// Encapsulates SMAPI's JSON file parsing. - public ContentCoordinator(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper) + /// A callback to invoke the first time *any* game content manager loads an asset. + public ContentCoordinator(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action onLoadingFirstAsset) { this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor)); this.Reflection = reflection; this.JsonHelper = jsonHelper; + this.OnLoadingFirstAsset = onLoadingFirstAsset; this.FullRootDirectory = Path.Combine(Constants.ExecutionPath, rootDirectory); this.ContentManagers.Add( - this.MainContentManager = new GameContentManager("Game1.content", serviceProvider, rootDirectory, currentCulture, this, monitor, reflection, this.OnDisposing) + this.MainContentManager = new GameContentManager("Game1.content", serviceProvider, rootDirectory, currentCulture, this, monitor, reflection, this.OnDisposing, onLoadingFirstAsset) ); this.CoreAssets = new CoreAssetPropagator(this.MainContentManager.AssertAndNormaliseAssetName, reflection, monitor); } @@ -88,7 +93,7 @@ namespace StardewModdingAPI.Framework /// A name for the mod manager. Not guaranteed to be unique. public GameContentManager CreateGameContentManager(string name) { - GameContentManager manager = new GameContentManager(name, this.MainContentManager.ServiceProvider, this.MainContentManager.RootDirectory, this.MainContentManager.CurrentCulture, this, this.Monitor, this.Reflection, this.OnDisposing); + GameContentManager manager = new GameContentManager(name, this.MainContentManager.ServiceProvider, this.MainContentManager.RootDirectory, this.MainContentManager.CurrentCulture, this, this.Monitor, this.Reflection, this.OnDisposing, this.OnLoadingFirstAsset); this.ContentManagers.Add(manager); return manager; } diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index ee940cc7..f159f035 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -28,6 +28,12 @@ namespace StardewModdingAPI.Framework.ContentManagers /// A lookup which indicates whether the asset is localisable (i.e. the filename contains the locale), if previously loaded. private readonly IDictionary IsLocalisableLookup; + /// Whether the next load is the first for any game content manager. + private static bool IsFirstLoad = true; + + /// A callback to invoke the first time *any* game content manager loads an asset. + private readonly Action OnLoadingFirstAsset; + /********* ** Public methods @@ -41,10 +47,12 @@ namespace StardewModdingAPI.Framework.ContentManagers /// Encapsulates monitoring and logging. /// Simplifies access to private code. /// A callback to invoke when the content manager is being disposed. - public GameContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, Action onDisposing) + /// A callback to invoke the first time *any* game content manager loads an asset. + public GameContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, Action onDisposing, Action onLoadingFirstAsset) : base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isModFolder: false) { this.IsLocalisableLookup = reflection.GetField>(this, "_localizedAsset").GetValue(); + this.OnLoadingFirstAsset = onLoadingFirstAsset; } /// Load an asset that has been processed by the content pipeline. @@ -53,6 +61,13 @@ namespace StardewModdingAPI.Framework.ContentManagers /// The language code for which to load content. public override T Load(string assetName, LanguageCode language) { + // raise first-load callback + if (GameContentManager.IsFirstLoad) + { + GameContentManager.IsFirstLoad = false; + this.OnLoadingFirstAsset(); + } + // normalise asset name assetName = this.AssertAndNormaliseAssetName(assetName); if (this.TryParseExplicitLanguageAssetKey(assetName, out string newAssetName, out LanguageCode newLanguage)) diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 2f0e0f05..f64af82b 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -209,8 +209,19 @@ namespace StardewModdingAPI.Framework AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => AssemblyLoader.ResolveAssembly(e.Name); // override game - SGame.ConstructorHack = new SGameConstructorHack(this.Monitor, this.Reflection, this.Toolkit.JsonHelper); - this.GameInstance = new SGame(this.Monitor, this.MonitorForGame, this.Reflection, this.EventManager, this.Toolkit.JsonHelper, this.ModRegistry, SCore.DeprecationManager, this.OnLocaleChanged, this.InitialiseAfterGameStart, this.Dispose); + SGame.ConstructorHack = new SGameConstructorHack(this.Monitor, this.Reflection, this.Toolkit.JsonHelper, this.InitialiseBeforeFirstAssetLoaded); + this.GameInstance = new SGame( + monitor: this.Monitor, + monitorForGame: this.MonitorForGame, + reflection: this.Reflection, + eventManager: this.EventManager, + jsonHelper: this.Toolkit.JsonHelper, + modRegistry: this.ModRegistry, + deprecationManager: SCore.DeprecationManager, + onLocaleChanged: this.OnLocaleChanged, + onGameInitialised: this.InitialiseAfterGameStart, + onGameExiting: this.Dispose + ); StardewValley.Program.gamePtr = this.GameInstance; // apply game patches @@ -280,6 +291,19 @@ namespace StardewModdingAPI.Framework File.Delete(Constants.FatalCrashMarker); } + // add headers + if (this.Settings.DeveloperMode) + this.Monitor.Log($"You have SMAPI for developers, so the console will be much more verbose. You can disable developer mode by installing the non-developer version of SMAPI, or by editing {Constants.ApiConfigPath}.", LogLevel.Info); + if (!this.Settings.CheckForUpdates) + this.Monitor.Log($"You configured SMAPI to not check for updates. Running an old version of SMAPI is not recommended. You can enable update checks by reinstalling SMAPI or editing {Constants.ApiConfigPath}.", LogLevel.Warn); + if (!this.Monitor.WriteToConsole) + this.Monitor.Log("Writing to the terminal is disabled because the --no-terminal argument was received. This usually means launching the terminal failed.", LogLevel.Warn); + this.Monitor.VerboseLog("Verbose logging enabled."); + + // update window titles + this.GameInstance.Window.Title = $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion}"; + Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion}"; + // start game this.Monitor.Log("Starting game...", LogLevel.Debug); try @@ -349,21 +373,14 @@ namespace StardewModdingAPI.Framework /********* ** Private methods *********/ - /// Initialise SMAPI and mods after the game starts. - private void InitialiseAfterGameStart() + /// Initialise mods before the first game asset is loaded. At this point the core content managers are loaded (so mods can load their own assets), but the game is mostly uninitialised. + private void InitialiseBeforeFirstAssetLoaded() { - // add headers - if (this.Settings.DeveloperMode) - this.Monitor.Log($"You have SMAPI for developers, so the console will be much more verbose. You can disable developer mode by installing the non-developer version of SMAPI, or by editing {Constants.ApiConfigPath}.", LogLevel.Info); - if (!this.Settings.CheckForUpdates) - this.Monitor.Log($"You configured SMAPI to not check for updates. Running an old version of SMAPI is not recommended. You can enable update checks by reinstalling SMAPI or editing {Constants.ApiConfigPath}.", LogLevel.Warn); - if (!this.Monitor.WriteToConsole) - this.Monitor.Log("Writing to the terminal is disabled because the --no-terminal argument was received. This usually means launching the terminal failed.", LogLevel.Warn); - this.Monitor.VerboseLog("Verbose logging enabled."); - - // validate XNB integrity - if (!this.ValidateContentIntegrity()) - this.Monitor.Log("SMAPI found problems in your game's content files which are likely to cause errors or crashes. Consider uninstalling XNB mods or reinstalling the game.", LogLevel.Error); + if (this.Monitor.IsExiting) + { + this.Monitor.Log("SMAPI shutting down: aborting initialisation.", LogLevel.Warn); + return; + } // load mod data ModToolkit toolkit = new ModToolkit(); @@ -404,16 +421,19 @@ namespace StardewModdingAPI.Framework // check for updates this.CheckForUpdatesAsync(mods); } - if (this.Monitor.IsExiting) - { - this.Monitor.Log("SMAPI shutting down: aborting initialisation.", LogLevel.Warn); - return; - } // update window titles int modsLoaded = this.ModRegistry.GetAll().Count(); this.GameInstance.Window.Title = $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion} with {modsLoaded} mods"; Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion} with {modsLoaded} mods"; + } + + /// Initialise SMAPI and mods after the game starts. + private void InitialiseAfterGameStart() + { + // validate XNB integrity + if (!this.ValidateContentIntegrity()) + this.Monitor.Log("SMAPI found problems in your game's content files which are likely to cause errors or crashes. Consider uninstalling XNB mods or reinstalling the game.", LogLevel.Error); // start SMAPI console new Thread(this.RunConsoleLoop).Start(); diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index b35e1d71..002a5d1b 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -75,6 +75,9 @@ namespace StardewModdingAPI.Framework /// A callback to invoke after the content language changes. private readonly Action OnLocaleChanged; + /// A callback to invoke the first time *any* game content manager loads an asset. + private readonly Action OnLoadingFirstAsset; + /// A callback to invoke after the game finishes initialising. private readonly Action OnGameInitialised; @@ -139,6 +142,7 @@ namespace StardewModdingAPI.Framework /// A callback to invoke when the game exits. internal SGame(IMonitor monitor, IMonitor monitorForGame, Reflector reflection, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, DeprecationManager deprecationManager, Action onLocaleChanged, Action onGameInitialised, Action onGameExiting) { + this.OnLoadingFirstAsset = SGame.ConstructorHack.OnLoadingFirstAsset; SGame.ConstructorHack = null; // check expectations @@ -237,7 +241,7 @@ namespace StardewModdingAPI.Framework // NOTE: this method is called before the SGame constructor runs. Don't depend on anything being initialised at this point. if (this.ContentCore == null) { - this.ContentCore = new ContentCoordinator(serviceProvider, rootDirectory, Thread.CurrentThread.CurrentUICulture, SGame.ConstructorHack.Monitor, SGame.ConstructorHack.Reflection, SGame.ConstructorHack.JsonHelper); + this.ContentCore = new ContentCoordinator(serviceProvider, rootDirectory, Thread.CurrentThread.CurrentUICulture, SGame.ConstructorHack.Monitor, SGame.ConstructorHack.Reflection, SGame.ConstructorHack.JsonHelper, this.OnLoadingFirstAsset ?? SGame.ConstructorHack?.OnLoadingFirstAsset); this.NextContentManagerIsMain = true; return this.ContentCore.CreateGameContentManager("Game1._temporaryContent"); } @@ -764,7 +768,10 @@ namespace StardewModdingAPI.Framework // game launched bool isFirstTick = SGame.TicksElapsed == 0; if (isFirstTick) + { + Context.IsGameLaunched = true; events.GameLaunched.Raise(new GameLaunchedEventArgs()); + } // preloaded if (Context.IsSaveLoaded && Context.LoadStage != LoadStage.Loaded && Context.LoadStage != LoadStage.Ready) diff --git a/src/SMAPI/Framework/SGameConstructorHack.cs b/src/SMAPI/Framework/SGameConstructorHack.cs index 494bab99..c3d22197 100644 --- a/src/SMAPI/Framework/SGameConstructorHack.cs +++ b/src/SMAPI/Framework/SGameConstructorHack.cs @@ -1,3 +1,4 @@ +using System; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Toolkit.Serialisation; using StardewValley; @@ -19,6 +20,9 @@ namespace StardewModdingAPI.Framework /// Encapsulates SMAPI's JSON file parsing. public JsonHelper JsonHelper { get; } + /// A callback to invoke the first time *any* game content manager loads an asset. + public Action OnLoadingFirstAsset { get; } + /********* ** Public methods @@ -27,11 +31,13 @@ namespace StardewModdingAPI.Framework /// Encapsulates monitoring and logging. /// Simplifies access to private game code. /// Encapsulates SMAPI's JSON file parsing. - public SGameConstructorHack(IMonitor monitor, Reflector reflection, JsonHelper jsonHelper) + /// A callback to invoke the first time *any* game content manager loads an asset. + public SGameConstructorHack(IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action onLoadingFirstAsset) { this.Monitor = monitor; this.Reflection = reflection; this.JsonHelper = jsonHelper; + this.OnLoadingFirstAsset = onLoadingFirstAsset; } } } -- cgit From 80741d3d2da39fed43e9208df85318de9bc7e1d2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 30 Mar 2019 01:32:54 -0400 Subject: update NuGet package version & release notes --- docs/mod-build-config.md | 2 ++ src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj | 6 ++++++ src/SMAPI.ModBuildConfig/package.nuspec | 6 +++--- 3 files changed, 11 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/docs/mod-build-config.md b/docs/mod-build-config.md index 14d167f2..5024aec5 100644 --- a/docs/mod-build-config.md +++ b/docs/mod-build-config.md @@ -214,7 +214,9 @@ _[Game path](#game-path)_ above. ## Release notes ### Upcoming release +* If the project contains an `assets` folder, its contents are now included in the mod automatically. * Dropped support for very old versions of SMAPI and Visual Studio. +* Fixed `Newtonsoft.Json.pdb` included in release zips when Json.NET is referenced directly. ### 2.2 * Added support for SMAPI 2.8+ (still compatible with earlier versions). diff --git a/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj b/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj index 44f0a3e7..870973a0 100644 --- a/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj +++ b/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj @@ -28,6 +28,12 @@
+ + + mod-build-config.md + + + diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec index b308413f..eb967bc0 100644 --- a/src/SMAPI.ModBuildConfig/package.nuspec +++ b/src/SMAPI.ModBuildConfig/package.nuspec @@ -2,7 +2,7 @@ Pathoschild.Stardew.ModBuildConfig - 2.2.1 + 3.0.0 Build package for SMAPI mods Pathoschild Pathoschild @@ -10,9 +10,9 @@ https://github.com/Pathoschild/SMAPI/blob/develop/LICENSE.txt https://github.com/Pathoschild/SMAPI/blob/develop/docs/mod-build-config.md#readme https://raw.githubusercontent.com/Pathoschild/SMAPI/develop/src/SMAPI.ModBuildConfig/assets/nuget-icon.png - Automates the build configuration for crossplatform Stardew Valley SMAPI mods. For Stardew Valley 1.3 or later. + Automates the build configuration for crossplatform Stardew Valley SMAPI mods. For SMAPI 2.11 or later. - 2.2.1: + 3.0.0: - If the project contains an `assets` folder, its contents are now included in the mod automatically. - Dropped support for very old versions of SMAPI and Visual Studio. - Fixed `Newtonsoft.Json.pdb` included in release zips when Json.NET is referenced directly. -- cgit From d10ded0fcc5e92f16d13a34e1d83925a1f99feb1 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 4 Apr 2019 00:10:03 -0400 Subject: update compatibility list --- docs/release-notes.md | 1 + .../wwwroot/StardewModdingAPI.metadata.json | 151 +++++++++++++++++++-- 2 files changed, 144 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 649cd774..e9819860 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ These changes have not been released yet. * For players: * Improved performance. + * Updated mod compatibility list. * Fixed Save Backup not pruning old backups if they're uncompressed. * Fixed issues when a farmhand reconnects before the game notices they're disconnected. * Fixed 'received message' logs shown in non-developer mode. diff --git a/src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json b/src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json index d0c55552..a3336ed3 100644 --- a/src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json +++ b/src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json @@ -59,15 +59,15 @@ "Default | UpdateKey": "Nexus:2270" }, - "Content Patcher": { - "ID": "Pathoschild.ContentPatcher", - "Default | UpdateKey": "Nexus:1915" - }, + //"Content Patcher": { + // "ID": "Pathoschild.ContentPatcher", + // "Default | UpdateKey": "Nexus:1915" + //}, - "Custom Farming Redux": { - "ID": "Platonymous.CustomFarming", - "Default | UpdateKey": "Nexus:991" - }, + //"Custom Farming Redux": { + // "ID": "Platonymous.CustomFarming", + // "Default | UpdateKey": "Nexus:991" + //}, "Custom Shirts": { "ID": "Platonymous.CustomShirts", @@ -234,6 +234,141 @@ "~ | StatusReasonPhrase": "debug mode was removed in SMAPI 1.0." }, + /********* + ** Broke in SMAPI 3.0 (runtime errors due to lifecycle changes) + *********/ + "Advancing Sprinklers": { + "ID": "warix3.advancingsprinklers", + "~1.0.0 | Status": "AssumeBroken" + }, + + "Arcade 2048": { + "ID": "Platonymous.2048", + "~1.0.6 | Status": "AssumeBroken" // possibly due to PyTK + }, + + "Arcade Snake": { + "ID": "Platonymous.Snake", + "~1.1.0 | Status": "AssumeBroken" // possibly due to PyTK + }, + + "Better Sprinklers": { + "ID": "Speeder.BetterSprinklers", + "~2.3.1-unofficial.7-pathoschild | Status": "AssumeBroken" + }, + + "Companion NPCs": { + "ID": "Redwood.CompanionNPCs", + "~0.0.9 | Status": "AssumeBroken" + }, + + "Content Patcher": { + "ID": "Pathoschild.ContentPatcher", + "Default | UpdateKey": "Nexus:1915", + "~1.6.4 | Status": "AssumeBroken" + }, + + "Crop Transplant Mod": { + "ID": "DIGUS.CropTransplantMod", + "~1.1.3 | Status": "AssumeBroken" + }, + + "Custom Adventure Guild Challenges": { + "ID": "DefenTheNation.CustomGuildChallenges", + "~1.8 | Status": "AssumeBroken" + }, + + "Custom Farming Redux": { + "ID": "Platonymous.CustomFarming", + "Default | UpdateKey": "Nexus:991", + "~2.10.10 | Status": "AssumeBroken" // possibly due to PyTK + }, + + "Deep Woods": { + "ID": "maxvollmer.deepwoodsmod", + "~1.5-beta.1 | Status": "AssumeBroken" + }, + + "Everlasting Baits and Unbreaking Tackles": { + "ID": "DIGUS.EverlastingBaitsAndUnbreakableTacklesMod", + "~1.2.4 | Status": "AssumeBroken" + }, + + "Farmhouse Redone": { + "ID": "mabelsyrup.farmhouse", + "~0.2 | Status": "AssumeBroken" + }, + + "Geode Info Menu": { + "ID": "cat.geodeinfomenu", + "~1.5 | Status": "AssumeBroken" + }, + + "Harp of Yoba Redux": { + "ID": "Platonymous.HarpOfYobaRedux", + "~2.6.3 | Status": "AssumeBroken" // possibly due to PyTK + }, + + "Infested Levels": { + "ID": "Eireon.InfestedLevels", + "~1.0.5 | Status": "AssumeBroken" + }, + + "JoJaBan - Arcade Sokoban": { + "ID": "Platonymous.JoJaBan", + "~0.4.3 | Status": "AssumeBroken" // possibly due to PyTK + }, + + "Level Extender": { + "ID": "DevinLematty.LevelExtender", + "~3.1 | Status": "AssumeBroken" + }, + + "Mod Update Menu": { + "ID": "cat.modupdatemenu", + "~1.4 | Status": "AssumeBroken" + }, + + "Mushroom Levels": { + "ID": "Eireon.MushroomLevels", + "~1.0.4 | Status": "AssumeBroken" + }, + + "Notes": { + "ID": "Platonymous.Notes", + "~1.0.5 | Status": "AssumeBroken" // possibly due to PyTK + }, + + "Quick Start": { + "ID": "WuestMan.QuickStart", + "~1.5 | Status": "AssumeBroken" + }, + + "Seed Bag": { + "ID": "Platonymous.SeedBag", + "~1.2.7 | Status": "AssumeBroken" // possibly due to PyTK + }, + + "Separate Money": { + "ID": "funnysnek.SeparateMoney", + "~1.4.2 | Status": "AssumeBroken" + }, + + "Split Money": { + "ID": "Platonymous.SplitMoney", + "~1.2.4 | Status": "AssumeBroken" // possibly due to PyTK + }, + + "Stack to Nearby Chests": { + "ID": "Ilyaki.StackToNearbyChests", + "~1.4.4 | Status": "AssumeBroken" + }, + + "Tree Transplant": { + "ID": "TreeTransplant", + "~1.0.5 | Status": "AssumeBroken" + }, + /********* ** Broke in SDV 1.3.36 *********/ -- cgit From 307055b028e0d6643983189e96a5ebcbe89d32ba Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 4 Apr 2019 18:43:56 -0400 Subject: bump version to 3.0 --- build/GlobalAssemblyInfo.cs | 4 ++-- src/SMAPI.Mods.ConsoleCommands/manifest.json | 4 ++-- src/SMAPI.Mods.SaveBackup/manifest.json | 4 ++-- src/SMAPI/Constants.cs | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) (limited to 'src') diff --git a/build/GlobalAssemblyInfo.cs b/build/GlobalAssemblyInfo.cs index a3ca3051..0fa0d331 100644 --- a/build/GlobalAssemblyInfo.cs +++ b/build/GlobalAssemblyInfo.cs @@ -1,5 +1,5 @@ using System.Reflection; [assembly: AssemblyProduct("SMAPI")] -[assembly: AssemblyVersion("2.11.3")] -[assembly: AssemblyFileVersion("2.11.3")] +[assembly: AssemblyVersion("3.0.0")] +[assembly: AssemblyFileVersion("3.0.0")] diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index 74295410..802de3a6 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.11.3", + "Version": "3.0.0", "Description": "Adds SMAPI console commands that let you manipulate the game.", "UniqueID": "SMAPI.ConsoleCommands", "EntryDll": "ConsoleCommands.dll", - "MinimumApiVersion": "2.11.3" + "MinimumApiVersion": "3.0.0" } diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json index e147bd39..eddd3443 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.11.3", + "Version": "3.0.0", "Description": "Automatically backs up all your saves once per day into its folder.", "UniqueID": "SMAPI.SaveBackup", "EntryDll": "SaveBackup.dll", - "MinimumApiVersion": "2.11.3" + "MinimumApiVersion": "3.0.0" } diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 736a10ea..3ba3e6dd 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -20,13 +20,13 @@ namespace StardewModdingAPI ** Public ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("2.11.3"); + public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.0.0"); /// The minimum supported version of Stardew Valley. - public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.3.36"); + public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.4.0"); /// The maximum supported version of Stardew Valley. - public static ISemanticVersion MaximumGameVersion { get; } = new GameVersion("1.3.36"); + public static ISemanticVersion MaximumGameVersion { get; } = null; /// The target game platform. public static GamePlatform TargetPlatform => (GamePlatform)Constants.Platform; -- cgit From 09d1c5a601e77c051dfde8a31f422b2c898086c3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 6 Apr 2019 00:35:53 -0400 Subject: list all detected issues in trace logs for incompatible mods --- docs/release-notes.md | 1 + src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 20 +++++++++---------- .../ReferenceToMemberWithUnexpectedTypeFinder.cs | 5 +---- .../ModLoading/IncompatibleInstructionException.cs | 23 ++++------------------ 4 files changed, 16 insertions(+), 33 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index e9819860..6aa3c024 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -14,6 +14,7 @@ These changes have not been released yet. * Added `IContentPack.HasFile` method. * Added `Context.IsGameLaunched` field. * Mods are now loaded much earlier in the game launch. This lets mods intercept any content asset, but the game is not fully initialised when `Entry` is called (use the `GameLaunched` event if you need to run code when the game is initialised). + * When a mod is incompatible, the trace logs now list all detected issues instead of the first one. * Dropped support for all deprecated APIs. * Updated to Json.NET 12.0.1. diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index 878b3148..ca171ae1 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -105,7 +105,7 @@ namespace StardewModdingAPI.Framework.ModLoading continue; // rewrite assembly - bool changed = this.RewriteAssembly(mod, assembly.Definition, assumeCompatible, loggedMessages, logPrefix: " "); + bool changed = this.RewriteAssembly(mod, assembly.Definition, loggedMessages, logPrefix: " "); // detect broken assembly reference foreach (AssemblyNameReference reference in assembly.Definition.MainModule.AssemblyReferences) @@ -114,7 +114,7 @@ namespace StardewModdingAPI.Framework.ModLoading { this.Monitor.LogOnce(loggedMessages, $" Broken code in {assembly.File.Name}: reference to missing assembly '{reference.FullName}'."); if (!assumeCompatible) - throw new IncompatibleInstructionException($"assembly reference to {reference.FullName}", $"Found a reference to missing assembly '{reference.FullName}' while loading assembly {assembly.File.Name}."); + throw new IncompatibleInstructionException($"Found a reference to missing assembly '{reference.FullName}' while loading assembly {assembly.File.Name}."); mod.SetWarning(ModWarning.BrokenCodeLoaded); break; } @@ -143,6 +143,10 @@ namespace StardewModdingAPI.Framework.ModLoading this.AssemblyDefinitionResolver.Add(assembly.Definition); } + // throw if incompatibilities detected + if (!assumeCompatible && mod.Warnings.HasFlag(ModWarning.BrokenCodeLoaded)) + throw new IncompatibleInstructionException(); + // last assembly loaded is the root return lastAssembly; } @@ -244,12 +248,11 @@ namespace StardewModdingAPI.Framework.ModLoading /// Rewrite the types referenced by an assembly. /// The mod for which the assembly is being loaded. /// The assembly to rewrite. - /// Assume the mod is compatible, even if incompatible code is detected. /// The messages that have already been logged for this mod. /// A string to prefix to log messages. /// Returns whether the assembly was modified. /// An incompatible CIL instruction was found while rewriting the assembly. - private bool RewriteAssembly(IModMetadata mod, AssemblyDefinition assembly, bool assumeCompatible, HashSet loggedMessages, string logPrefix) + private bool RewriteAssembly(IModMetadata mod, AssemblyDefinition assembly, HashSet loggedMessages, string logPrefix) { ModuleDefinition module = assembly.MainModule; string filename = $"{assembly.Name.Name}.dll"; @@ -288,7 +291,7 @@ namespace StardewModdingAPI.Framework.ModLoading foreach (IInstructionHandler handler in handlers) { InstructionHandleResult result = handler.Handle(module, method, this.AssemblyMap, platformChanged); - this.ProcessInstructionHandleResult(mod, handler, result, loggedMessages, logPrefix, assumeCompatible, filename); + this.ProcessInstructionHandleResult(mod, handler, result, loggedMessages, logPrefix, filename); if (result == InstructionHandleResult.Rewritten) anyRewritten = true; } @@ -303,7 +306,7 @@ namespace StardewModdingAPI.Framework.ModLoading { Instruction instruction = instructions[offset]; InstructionHandleResult result = handler.Handle(module, cil, instruction, this.AssemblyMap, platformChanged); - this.ProcessInstructionHandleResult(mod, handler, result, loggedMessages, logPrefix, assumeCompatible, filename); + this.ProcessInstructionHandleResult(mod, handler, result, loggedMessages, logPrefix, filename); if (result == InstructionHandleResult.Rewritten) anyRewritten = true; } @@ -318,10 +321,9 @@ namespace StardewModdingAPI.Framework.ModLoading /// The instruction handler. /// The result returned by the handler. /// The messages already logged for the current mod. - /// Assume the mod is compatible, even if incompatible code is detected. /// A string to prefix to log messages. /// The assembly filename for log messages. - private void ProcessInstructionHandleResult(IModMetadata mod, IInstructionHandler handler, InstructionHandleResult result, HashSet loggedMessages, string logPrefix, bool assumeCompatible, string filename) + private void ProcessInstructionHandleResult(IModMetadata mod, IInstructionHandler handler, InstructionHandleResult result, HashSet loggedMessages, string logPrefix, string filename) { switch (result) { @@ -331,8 +333,6 @@ namespace StardewModdingAPI.Framework.ModLoading case InstructionHandleResult.NotCompatible: this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Broken code in {filename}: {handler.NounPhrase}."); - if (!assumeCompatible) - throw new IncompatibleInstructionException(handler.NounPhrase, $"Found an incompatible CIL instruction ({handler.NounPhrase}) while loading assembly {filename}."); mod.SetWarning(ModWarning.BrokenCodeLoaded); break; diff --git a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs index 82c4920a..459e3210 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs @@ -80,10 +80,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders // compare return types MethodDefinition methodDef = methodReference.Resolve(); if (methodDef == null) - { - this.NounPhrase = $"reference to {methodReference.DeclaringType.FullName}.{methodReference.Name} (no such method)"; - return InstructionHandleResult.NotCompatible; - } + return InstructionHandleResult.None; // validated by ReferenceToMissingMemberFinder if (candidateMethods.All(method => !RewriteHelper.LooksLikeSameType(method.ReturnType, methodDef.ReturnType))) { diff --git a/src/SMAPI/Framework/ModLoading/IncompatibleInstructionException.cs b/src/SMAPI/Framework/ModLoading/IncompatibleInstructionException.cs index 17ec24b1..1f9add30 100644 --- a/src/SMAPI/Framework/ModLoading/IncompatibleInstructionException.cs +++ b/src/SMAPI/Framework/ModLoading/IncompatibleInstructionException.cs @@ -5,31 +5,16 @@ namespace StardewModdingAPI.Framework.ModLoading /// An exception raised when an incompatible instruction is found while loading a mod assembly. internal class IncompatibleInstructionException : Exception { - /********* - ** Accessors - *********/ - /// A brief noun phrase which describes the incompatible instruction that was found. - public string NounPhrase { get; } - - /********* ** Public methods *********/ /// Construct an instance. - /// A brief noun phrase which describes the incompatible instruction that was found. - public IncompatibleInstructionException(string nounPhrase) - : base($"Found an incompatible CIL instruction ({nounPhrase}).") - { - this.NounPhrase = nounPhrase; - } + public IncompatibleInstructionException() + : base("Found incompatible CIL instructions.") { } /// Construct an instance. - /// A brief noun phrase which describes the incompatible instruction that was found. /// A message which describes the error. - public IncompatibleInstructionException(string nounPhrase, string message) - : base(message) - { - this.NounPhrase = nounPhrase; - } + public IncompatibleInstructionException(string message) + : base(message) { } } } -- cgit From 6c220453e16c6cb5ad150b61cf02685a97557b3c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 15 Apr 2019 00:40:45 -0400 Subject: fix translatable assets not updated when switching language (#586) --- docs/release-notes.md | 1 + src/SMAPI/Framework/ContentCoordinator.cs | 7 +++++++ .../ContentManagers/BaseContentManager.cs | 3 +++ .../ContentManagers/GameContentManager.cs | 23 ++++++++++++++++++++++ .../Framework/ContentManagers/IContentManager.cs | 3 +++ src/SMAPI/Framework/SCore.cs | 6 +++++- src/SMAPI/Framework/SGame.cs | 8 +------- 7 files changed, 43 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 6aa3c024..84738ff7 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,7 @@ These changes have not been released yet. * Fixed Save Backup not pruning old backups if they're uncompressed. * Fixed issues when a farmhand reconnects before the game notices they're disconnected. * Fixed 'received message' logs shown in non-developer mode. + * Fixed some assets not updated when you switch language to English. * For modders: * Added support for content pack translations. diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index caeb7ee9..25eeb2ef 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -114,6 +114,13 @@ namespace StardewModdingAPI.Framework return this.MainContentManager.GetLocale(LocalizedContentManager.CurrentLanguageCode); } + /// Perform any cleanup needed when the locale changes. + public void OnLocaleChanged() + { + foreach (IContentManager contentManager in this.ContentManagers) + contentManager.OnLocaleChanged(); + } + /// Get whether this asset is mapped to a mod folder. /// The asset key. public bool IsManagedAssetKey(string key) diff --git a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs index 7821e454..b2b3769b 100644 --- a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs @@ -138,6 +138,9 @@ namespace StardewModdingAPI.Framework.ContentManagers } } + /// Perform any cleanup needed when the locale changes. + public virtual void OnLocaleChanged() { } + /// Normalise path separators in a file path. For asset keys, see instead. /// The file path to normalise. [Pure] diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index f159f035..55cf15ec 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -112,6 +112,29 @@ namespace StardewModdingAPI.Framework.ContentManagers return data; } + /// Perform any cleanup needed when the locale changes. + public override void OnLocaleChanged() + { + base.OnLocaleChanged(); + + // find assets for which a translatable version was loaded + HashSet removeAssetNames = new HashSet(StringComparer.InvariantCultureIgnoreCase); + foreach (string key in this.IsLocalisableLookup.Where(p => p.Value).Select(p => p.Key)) + removeAssetNames.Add(this.TryParseExplicitLanguageAssetKey(key, out string assetName, out _) ? assetName : key); + + // invalidate translatable assets + string[] invalidated = this + .InvalidateCache((key, type) => + removeAssetNames.Contains(key) + || (this.TryParseExplicitLanguageAssetKey(key, out string assetName, out _) && removeAssetNames.Contains(assetName)) + ) + .Select(p => p.Item1) + .OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase) + .ToArray(); + if (invalidated.Any()) + this.Monitor.Log($"Invalidated {invalidated.Length} asset names: {string.Join(", ", invalidated)} for locale change.", LogLevel.Trace); + } + /// Create a new content manager for temporary use. public override LocalizedContentManager CreateTemporary() { diff --git a/src/SMAPI/Framework/ContentManagers/IContentManager.cs b/src/SMAPI/Framework/ContentManagers/IContentManager.cs index 17618edd..66ef9181 100644 --- a/src/SMAPI/Framework/ContentManagers/IContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/IContentManager.cs @@ -52,6 +52,9 @@ namespace StardewModdingAPI.Framework.ContentManagers /// The asset to clone. T CloneIfPossible(T asset); + /// Perform any cleanup needed when the locale changes. + void OnLocaleChanged(); + /// Normalise path separators in a file path. For asset keys, see instead. /// The file path to normalise. [Pure] diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index f64af82b..e8d5b672 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -208,6 +208,9 @@ namespace StardewModdingAPI.Framework // add more leniant assembly resolvers AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => AssemblyLoader.ResolveAssembly(e.Name); + // hook locale event + LocalizedContentManager.OnLanguageChange += locale => this.OnLocaleChanged(); + // override game SGame.ConstructorHack = new SGameConstructorHack(this.Monitor, this.Reflection, this.Toolkit.JsonHelper, this.InitialiseBeforeFirstAssetLoaded); this.GameInstance = new SGame( @@ -218,7 +221,6 @@ namespace StardewModdingAPI.Framework jsonHelper: this.Toolkit.JsonHelper, modRegistry: this.ModRegistry, deprecationManager: SCore.DeprecationManager, - onLocaleChanged: this.OnLocaleChanged, onGameInitialised: this.InitialiseAfterGameStart, onGameExiting: this.Dispose ); @@ -442,6 +444,8 @@ namespace StardewModdingAPI.Framework /// Handle the game changing locale. private void OnLocaleChanged() { + this.ContentCore.OnLocaleChanged(); + // get locale string locale = this.ContentCore.GetLocale(); LocalizedContentManager.LanguageCode languageCode = this.ContentCore.Language; diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 002a5d1b..684ff3ba 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -72,9 +72,6 @@ namespace StardewModdingAPI.Framework /// Whether the game is creating the save file and SMAPI has already raised . private bool IsBetweenCreateEvents; - /// A callback to invoke after the content language changes. - private readonly Action OnLocaleChanged; - /// A callback to invoke the first time *any* game content manager loads an asset. private readonly Action OnLoadingFirstAsset; @@ -137,10 +134,9 @@ namespace StardewModdingAPI.Framework /// Encapsulates SMAPI's JSON file parsing. /// Tracks the installed mods. /// Manages deprecation warnings. - /// A callback to invoke after the content language changes. /// A callback to invoke after the game finishes initialising. /// A callback to invoke when the game exits. - internal SGame(IMonitor monitor, IMonitor monitorForGame, Reflector reflection, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, DeprecationManager deprecationManager, Action onLocaleChanged, Action onGameInitialised, Action onGameExiting) + internal SGame(IMonitor monitor, IMonitor monitorForGame, Reflector reflection, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, DeprecationManager deprecationManager, Action onGameInitialised, Action onGameExiting) { this.OnLoadingFirstAsset = SGame.ConstructorHack.OnLoadingFirstAsset; SGame.ConstructorHack = null; @@ -159,7 +155,6 @@ namespace StardewModdingAPI.Framework this.ModRegistry = modRegistry; this.Reflection = reflection; this.DeprecationManager = deprecationManager; - this.OnLocaleChanged = onLocaleChanged; this.OnGameInitialised = onGameInitialised; this.OnGameExiting = onGameExiting; Game1.input = new SInputState(); @@ -467,7 +462,6 @@ namespace StardewModdingAPI.Framework if (this.Watchers.LocaleWatcher.IsChanged) { this.Monitor.Log($"Context: locale set to {this.Watchers.LocaleWatcher.CurrentValue}.", LogLevel.Trace); - this.OnLocaleChanged(); this.Watchers.LocaleWatcher.Reset(); } -- cgit From 161618d0d92bad5b31e2780c8899c73339d38b62 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 15 Apr 2019 21:45:27 -0400 Subject: fix cache miss when not playing in English (#634) --- .../ContentManagers/BaseContentManager.cs | 4 +-- .../ContentManagers/GameContentManager.cs | 30 ++++++++++++++++++---- .../Framework/ContentManagers/IContentManager.cs | 5 ++-- .../Framework/ContentManagers/ModContentManager.cs | 4 +-- src/SMAPI/Framework/ModHelpers/ContentHelper.cs | 2 +- 5 files changed, 32 insertions(+), 13 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs index b2b3769b..f54262b8 100644 --- a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs @@ -103,11 +103,11 @@ namespace StardewModdingAPI.Framework.ContentManagers /// The type of asset to inject. /// The asset path relative to the loader root directory, not including the .xnb extension. /// The asset value. - public void Inject(string assetName, T value) + /// The language code for which to inject the asset. + public virtual void Inject(string assetName, T value, LanguageCode language) { assetName = this.AssertAndNormaliseAssetName(assetName); this.Cache[assetName] = value; - } /// Get a copy of the given asset if supported. diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index 55cf15ec..6ce50f00 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -81,7 +81,7 @@ namespace StardewModdingAPI.Framework.ContentManagers if (this.Coordinator.TryParseManagedAssetKey(assetName, out string contentManagerID, out string relativePath)) { T managedAsset = this.Coordinator.LoadAndCloneManagedAsset(assetName, contentManagerID, relativePath, language); - this.Inject(assetName, managedAsset); + this.Inject(assetName, managedAsset, language); return managedAsset; } @@ -108,10 +108,30 @@ namespace StardewModdingAPI.Framework.ContentManagers } // update cache & return data - this.Inject(assetName, data); + this.Inject(assetName, data, language); return data; } + /// Inject an asset into the cache. + /// The type of asset to inject. + /// The asset path relative to the loader root directory, not including the .xnb extension. + /// The asset value. + /// The language code for which to inject the asset. + public override void Inject(string assetName, T value, LanguageCode language) + { + base.Inject(assetName, value, language); + + // track whether the injected asset is translatable for is-loaded lookups + bool isTranslated = this.TryParseExplicitLanguageAssetKey(assetName, out _, out _); + string localisedKey = isTranslated ? assetName : $"{assetName}.{language}"; + if (this.Cache.ContainsKey(localisedKey)) + this.IsLocalisableLookup[localisedKey] = true; + else if (!isTranslated && this.Cache.ContainsKey(assetName)) + this.IsLocalisableLookup[localisedKey] = false; + else + this.Monitor.Log($"Asset '{assetName}' could not be found in the cache immediately after injection.", LogLevel.Error); + } + /// Perform any cleanup needed when the locale changes. public override void OnLocaleChanged() { @@ -154,11 +174,11 @@ namespace StardewModdingAPI.Framework.ContentManagers return this.Cache.ContainsKey(normalisedAssetName); // translated - string localeKey = $"{normalisedAssetName}.{this.GetLocale(this.GetCurrentLanguage())}"; - if (this.IsLocalisableLookup.TryGetValue(localeKey, out bool localisable)) + string localisedKey = $"{normalisedAssetName}.{this.GetLocale(this.GetCurrentLanguage())}"; + if (this.IsLocalisableLookup.TryGetValue(localisedKey, out bool localisable)) { return localisable - ? this.Cache.ContainsKey(localeKey) + ? this.Cache.ContainsKey(localisedKey) : this.Cache.ContainsKey(normalisedAssetName); } diff --git a/src/SMAPI/Framework/ContentManagers/IContentManager.cs b/src/SMAPI/Framework/ContentManagers/IContentManager.cs index 66ef9181..4c362b0a 100644 --- a/src/SMAPI/Framework/ContentManagers/IContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/IContentManager.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using Microsoft.Xna.Framework.Content; using StardewModdingAPI.Framework.Exceptions; @@ -45,7 +44,8 @@ namespace StardewModdingAPI.Framework.ContentManagers /// The type of asset to inject. /// The asset path relative to the loader root directory, not including the .xnb extension. /// The asset value. - void Inject(string assetName, T value); + /// The language code for which to inject the asset. + void Inject(string assetName, T value, LocalizedContentManager.LanguageCode language); /// Get a copy of the given asset if supported. /// The asset type. @@ -63,7 +63,6 @@ namespace StardewModdingAPI.Framework.ContentManagers /// Assert that the given key has a valid format and return a normalised form consistent with the underlying cache. /// The asset key to check. /// The asset key is empty or contains invalid characters. - [SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local", Justification = "Parameter is only used for assertion checks by design.")] string AssertAndNormaliseAssetName(string assetName); /// Get the current content locale. diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index 2c50ec04..1df7d107 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -57,7 +57,7 @@ namespace StardewModdingAPI.Framework.ContentManagers if (contentManagerID != this.Name) { T data = this.Coordinator.LoadAndCloneManagedAsset(assetName, contentManagerID, relativePath, language); - this.Inject(assetName, data); + this.Inject(assetName, data, language); return data; } @@ -127,7 +127,7 @@ namespace StardewModdingAPI.Framework.ContentManagers { Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream); texture = this.PremultiplyTransparency(texture); - this.Inject(internalKey, texture); + this.Inject(internalKey, texture, language); return (T)(object)texture; } diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs index 8b86fdeb..e02748d2 100644 --- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs @@ -126,7 +126,7 @@ namespace StardewModdingAPI.Framework.ModHelpers this.FixCustomTilesheetPaths(map, relativeMapPath: key); // inject map - this.ModContentManager.Inject(internalKey, map); + this.ModContentManager.Inject(internalKey, map, this.CurrentLocaleConstant); return (T)(object)map; } -- cgit From ee132828437c156207ddfc1cbc15339a6852c2fe Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 15 Apr 2019 22:13:10 -0400 Subject: update for new DLL in SDV 1.4 (#638) --- docs/mod-build-config.md | 1 + src/SMAPI.ModBuildConfig/build/smapi.targets | 10 ++++++++++ src/SMAPI.ModBuildConfig/package.nuspec | 1 + 3 files changed, 12 insertions(+) (limited to 'src') diff --git a/docs/mod-build-config.md b/docs/mod-build-config.md index 5024aec5..8bd7b79d 100644 --- a/docs/mod-build-config.md +++ b/docs/mod-build-config.md @@ -214,6 +214,7 @@ _[Game path](#game-path)_ above. ## Release notes ### Upcoming release +* Updated for Stardew Valley 1.4. * If the project contains an `assets` folder, its contents are now included in the mod automatically. * Dropped support for very old versions of SMAPI and Visual Studio. * Fixed `Newtonsoft.Json.pdb` included in release zips when Json.NET is referenced directly. diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets index 99011629..e3211466 100644 --- a/src/SMAPI.ModBuildConfig/build/smapi.targets +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -95,6 +95,11 @@ false true + + $(GamePath)\StardewValley.GameData.dll + false + true + $(GamePath)\StardewModdingAPI.exe false @@ -134,6 +139,11 @@ false true + + $(GamePath)\StardewValley.GameData.dll + false + true + $(GamePath)\StardewModdingAPI.exe false diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec index eb967bc0..b6ef98f0 100644 --- a/src/SMAPI.ModBuildConfig/package.nuspec +++ b/src/SMAPI.ModBuildConfig/package.nuspec @@ -13,6 +13,7 @@ Automates the build configuration for crossplatform Stardew Valley SMAPI mods. For SMAPI 2.11 or later. 3.0.0: + - Updated for Stardew Valley 1.4. - If the project contains an `assets` folder, its contents are now included in the mod automatically. - Dropped support for very old versions of SMAPI and Visual Studio. - Fixed `Newtonsoft.Json.pdb` included in release zips when Json.NET is referenced directly. -- cgit From 78f28357e4f87ed619144229ea65c1e1cb0f9dd3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 15 Apr 2019 22:36:50 -0400 Subject: update code for SDV 1.4 (#638) --- docs/release-notes.md | 1 + .../Framework/Commands/Player/SetMoneyCommand.cs | 4 +- .../Framework/ItemData/ItemType.cs | 17 +-- .../Framework/ItemRepository.cs | 4 + src/SMAPI/Framework/SGame.cs | 120 ++++++++------------- src/SMAPI/Metadata/CoreAssetPropagator.cs | 4 +- 6 files changed, 64 insertions(+), 86 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 84738ff7..32f4db37 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -3,6 +3,7 @@ These changes have not been released yet. * For players: + * Updated for Stardew Valley 1.4. * Improved performance. * Updated mod compatibility list. * Fixed Save Backup not pruning old backups if they're uncompressed. diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMoneyCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMoneyCommand.cs index ad11cc66..1706bbc1 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMoneyCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMoneyCommand.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player @@ -65,7 +65,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player public override void Update(IMonitor monitor) { if (this.InfiniteMoney) - Game1.player.money = 999999; + Game1.player.Money = 999999; } } } diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemData/ItemType.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemData/ItemType.cs index 7ee662d0..5d269c89 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemData/ItemType.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemData/ItemType.cs @@ -6,28 +6,31 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData /// A big craftable object in BigCraftable, - /// A item. + /// A item. Boots, - /// A flooring item. + /// A item. + Clothing, + + /// A flooring item. Flooring, - /// A item. + /// A item. Furniture, - /// A item. + /// A item. Hat, /// Any object in (except rings). Object, - /// A item. + /// A item. Ring, - /// A tool. + /// A tool. Tool, - /// A wall item. + /// A wall item. Wallpaper, /// A or item. diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs index fc631826..90cdb872 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs @@ -35,6 +35,10 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework yield return new SearchableItem(ItemType.Tool, this.CustomIDOffset + 2, new Pan()); yield return new SearchableItem(ItemType.Tool, this.CustomIDOffset + 3, new Wand()); + // clothing + foreach (int id in Game1.clothingInformation.Keys) + yield return new SearchableItem(ItemType.Clothing, id, new Clothing(id)); + // wallpapers for (int id = 0; id < 112; id++) yield return new SearchableItem(ItemType.Wallpaper, id, new Wallpaper(id) { Category = SObject.furnitureCategory }); diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 684ff3ba..c06f62cf 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -22,6 +22,7 @@ using StardewModdingAPI.Toolkit.Serialisation; using StardewValley; using StardewValley.BellsAndWhistles; using StardewValley.Buildings; +using StardewValley.Events; using StardewValley.Locations; using StardewValley.Menus; using StardewValley.TerrainFeatures; @@ -864,14 +865,14 @@ namespace StardewModdingAPI.Framework var events = this.Events; if (Game1._newDayTask != null) - this.GraphicsDevice.Clear(this.bgColor); + this.GraphicsDevice.Clear(Game1.bgColor); else { if ((double)Game1.options.zoomLevel != 1.0) this.GraphicsDevice.SetRenderTarget(this.screen); if (this.IsSaving) { - this.GraphicsDevice.Clear(this.bgColor); + this.GraphicsDevice.Clear(Game1.bgColor); IClickableMenu activeClickableMenu = Game1.activeClickableMenu; if (activeClickableMenu != null) { @@ -901,7 +902,7 @@ namespace StardewModdingAPI.Framework } else { - this.GraphicsDevice.Clear(this.bgColor); + this.GraphicsDevice.Clear(Game1.bgColor); if (Game1.activeClickableMenu != null && Game1.options.showMenuBackground && Game1.activeClickableMenu.showWithoutTransparencyIfOptionIsSet()) { Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); @@ -925,7 +926,7 @@ namespace StardewModdingAPI.Framework if ((double)Game1.options.zoomLevel != 1.0) { this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); - this.GraphicsDevice.Clear(this.bgColor); + this.GraphicsDevice.Clear(Game1.bgColor); Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); Game1.spriteBatch.Draw((Texture2D)this.screen, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screen.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); Game1.spriteBatch.End(); @@ -959,7 +960,7 @@ namespace StardewModdingAPI.Framework if ((double)Game1.options.zoomLevel == 1.0) return; this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); - this.GraphicsDevice.Clear(this.bgColor); + this.GraphicsDevice.Clear(Game1.bgColor); Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); Game1.spriteBatch.Draw((Texture2D)this.screen, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screen.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); Game1.spriteBatch.End(); @@ -988,7 +989,7 @@ namespace StardewModdingAPI.Framework if ((double)Game1.options.zoomLevel == 1.0) return; this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); - this.GraphicsDevice.Clear(this.bgColor); + this.GraphicsDevice.Clear(Game1.bgColor); Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); Game1.spriteBatch.Draw((Texture2D)this.screen, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screen.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); Game1.spriteBatch.End(); @@ -1003,7 +1004,7 @@ namespace StardewModdingAPI.Framework string str2 = Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3688"); string s = str2 + str1; string str3 = str2 + "... "; - int widthOfString = SpriteText.getWidthOfString(str3); + int widthOfString = SpriteText.getWidthOfString(str3, 999999); int height = 64; int x = 64; int y = Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Bottom - height; @@ -1014,7 +1015,7 @@ namespace StardewModdingAPI.Framework if ((double)Game1.options.zoomLevel != 1.0) { this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); - this.GraphicsDevice.Clear(this.bgColor); + this.GraphicsDevice.Clear(Game1.bgColor); Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); Game1.spriteBatch.Draw((Texture2D)this.screen, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screen.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); Game1.spriteBatch.End(); @@ -1031,7 +1032,6 @@ namespace StardewModdingAPI.Framework { byte batchOpens = 0; // used for rendering event - Microsoft.Xna.Framework.Rectangle rectangle; Viewport viewport; if (Game1.gameMode == (byte)0) { @@ -1052,14 +1052,27 @@ namespace StardewModdingAPI.Framework for (int index = 0; index < Game1.currentLightSources.Count; ++index) { if (Utility.isOnScreen((Vector2)((NetFieldBase)Game1.currentLightSources.ElementAt(index).position), (int)((double)(float)((NetFieldBase)Game1.currentLightSources.ElementAt(index).radius) * 64.0 * 4.0))) - Game1.spriteBatch.Draw(Game1.currentLightSources.ElementAt(index).lightTexture, Game1.GlobalToLocal(Game1.viewport, (Vector2)((NetFieldBase)Game1.currentLightSources.ElementAt(index).position)) / (float)(Game1.options.lightingQuality / 2), new Microsoft.Xna.Framework.Rectangle?(Game1.currentLightSources.ElementAt(index).lightTexture.Bounds), (Color)((NetFieldBase)Game1.currentLightSources.ElementAt(index).color), 0.0f, new Vector2((float)Game1.currentLightSources.ElementAt(index).lightTexture.Bounds.Center.X, (float)Game1.currentLightSources.ElementAt(index).lightTexture.Bounds.Center.Y), (float)((NetFieldBase)Game1.currentLightSources.ElementAt(index).radius) / (float)(Game1.options.lightingQuality / 2), SpriteEffects.None, 0.9f); + { + SpriteBatch spriteBatch = Game1.spriteBatch; + Texture2D lightTexture = Game1.currentLightSources.ElementAt(index).lightTexture; + Vector2 position = Game1.GlobalToLocal(Game1.viewport, (Vector2)((NetFieldBase)Game1.currentLightSources.ElementAt(index).position)) / (float)(Game1.options.lightingQuality / 2); + Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.currentLightSources.ElementAt(index).lightTexture.Bounds); + Color color = (Color)((NetFieldBase)Game1.currentLightSources.ElementAt(index).color); + Microsoft.Xna.Framework.Rectangle bounds = Game1.currentLightSources.ElementAt(index).lightTexture.Bounds; + double x = (double)bounds.Center.X; + bounds = Game1.currentLightSources.ElementAt(index).lightTexture.Bounds; + double y = (double)bounds.Center.Y; + Vector2 origin = new Vector2((float)x, (float)y); + double num = (double)(float)((NetFieldBase)Game1.currentLightSources.ElementAt(index).radius) / (double)(Game1.options.lightingQuality / 2); + spriteBatch.Draw(lightTexture, position, sourceRectangle, color, 0.0f, origin, (float)num, SpriteEffects.None, 0.9f); + } } Game1.spriteBatch.End(); this.GraphicsDevice.SetRenderTarget((double)Game1.options.zoomLevel == 1.0 ? (RenderTarget2D)null : this.screen); } if (Game1.bloomDay && Game1.bloom != null) Game1.bloom.BeginDraw(); - this.GraphicsDevice.Clear(this.bgColor); + this.GraphicsDevice.Clear(Game1.bgColor); Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); if (++batchOpens == 1) events.Rendering.RaiseEmpty(); @@ -1113,16 +1126,13 @@ namespace StardewModdingAPI.Framework Vector2 local = Game1.GlobalToLocal(farmerShadow.Position + new Vector2(32f, 24f)); Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds); Color white = Color.White; - double num1 = 0.0; Microsoft.Xna.Framework.Rectangle bounds = Game1.shadowTexture.Bounds; double x = (double)bounds.Center.X; bounds = Game1.shadowTexture.Bounds; double y = (double)bounds.Center.Y; Vector2 origin = new Vector2((float)x, (float)y); - double num2 = 4.0 - (!farmerShadow.running && !farmerShadow.UsingTool || farmerShadow.FarmerSprite.currentAnimationIndex <= 1 ? 0.0 : (double)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[farmerShadow.FarmerSprite.CurrentFrame]) * 0.5); - int num3 = 0; - double num4 = 0.0; - spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num1, origin, (float)num2, (SpriteEffects)num3, (float)num4); + double num = 4.0 - (!farmerShadow.running && !farmerShadow.UsingTool || farmerShadow.FarmerSprite.currentAnimationIndex <= 1 ? 0.0 : (double)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[farmerShadow.FarmerSprite.CurrentFrame]) * 0.5); + spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, 0.0f, origin, (float)num, SpriteEffects.None, 0.0f); } } } @@ -1136,7 +1146,7 @@ namespace StardewModdingAPI.Framework { foreach (NPC character in Game1.currentLocation.characters) { - if (!(bool)((NetFieldBase)character.swimming) && !character.HideShadow && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(character.getTileLocation())) + if (!(bool)((NetFieldBase)character.swimming) && !character.HideShadow && (!(bool)((NetFieldBase)character.isInvisible) && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(character.getTileLocation()))) Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, character.Position + new Vector2((float)(character.Sprite.SpriteWidth * 4) / 2f, (float)(character.GetBoundingBox().Height + (character.IsMonster ? 0 : 12)))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)character.yJumpOffset / 40.0) * (float)((NetFieldBase)character.scale), SpriteEffects.None, Math.Max(0.0f, (float)character.getStandingY() / 10000f) - 1E-06f); } } @@ -1157,16 +1167,13 @@ namespace StardewModdingAPI.Framework Vector2 local = Game1.GlobalToLocal(farmerShadow.Position + new Vector2(32f, 24f)); Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds); Color white = Color.White; - double num1 = 0.0; Microsoft.Xna.Framework.Rectangle bounds = Game1.shadowTexture.Bounds; double x = (double)bounds.Center.X; bounds = Game1.shadowTexture.Bounds; double y = (double)bounds.Center.Y; Vector2 origin = new Vector2((float)x, (float)y); - double num2 = 4.0 - (!farmerShadow.running && !farmerShadow.UsingTool || farmerShadow.FarmerSprite.currentAnimationIndex <= 1 ? 0.0 : (double)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[farmerShadow.FarmerSprite.CurrentFrame]) * 0.5); - int num3 = 0; - double num4 = 0.0; - spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num1, origin, (float)num2, (SpriteEffects)num3, (float)num4); + double num = 4.0 - (!farmerShadow.running && !farmerShadow.UsingTool || farmerShadow.FarmerSprite.currentAnimationIndex <= 1 ? 0.0 : (double)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[farmerShadow.FarmerSprite.CurrentFrame]) * 0.5); + spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, 0.0f, origin, (float)num, SpriteEffects.None, 0.0f); } } } @@ -1199,29 +1206,8 @@ namespace StardewModdingAPI.Framework Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); if (Game1.displayFarmer && Game1.player.ActiveObject != null && ((bool)((NetFieldBase)Game1.player.ActiveObject.bigCraftable) && this.checkBigCraftableBoundariesForFrontLayer()) && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), Game1.player.getStandingY()), Game1.viewport.Size) == null) Game1.drawPlayerHeldObject(Game1.player); - else if (Game1.displayFarmer && Game1.player.ActiveObject != null) - { - if (Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location((int)Game1.player.Position.X, (int)Game1.player.Position.Y - 38), Game1.viewport.Size) == null || Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location((int)Game1.player.Position.X, (int)Game1.player.Position.Y - 38), Game1.viewport.Size).TileIndexProperties.ContainsKey("FrontAlways")) - { - Layer layer1 = Game1.currentLocation.Map.GetLayer("Front"); - rectangle = Game1.player.GetBoundingBox(); - Location mapDisplayLocation1 = new Location(rectangle.Right, (int)Game1.player.Position.Y - 38); - Size size1 = Game1.viewport.Size; - if (layer1.PickTile(mapDisplayLocation1, size1) != null) - { - Layer layer2 = Game1.currentLocation.Map.GetLayer("Front"); - rectangle = Game1.player.GetBoundingBox(); - Location mapDisplayLocation2 = new Location(rectangle.Right, (int)Game1.player.Position.Y - 38); - Size size2 = Game1.viewport.Size; - if (layer2.PickTile(mapDisplayLocation2, size2).TileIndexProperties.ContainsKey("FrontAlways")) - goto label_129; - } - else - goto label_129; - } + else if (Game1.displayFarmer && Game1.player.ActiveObject != null && (Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location((int)Game1.player.Position.X, (int)Game1.player.Position.Y - 38), Game1.viewport.Size) != null && !Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location((int)Game1.player.Position.X, (int)Game1.player.Position.Y - 38), Game1.viewport.Size).TileIndexProperties.ContainsKey("FrontAlways") || Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.GetBoundingBox().Right, (int)Game1.player.Position.Y - 38), Game1.viewport.Size) != null && !Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.GetBoundingBox().Right, (int)Game1.player.Position.Y - 38), Game1.viewport.Size).TileIndexProperties.ContainsKey("FrontAlways"))) Game1.drawPlayerHeldObject(Game1.player); - } - label_129: if ((Game1.player.UsingTool || Game1.pickingTool) && Game1.player.CurrentTool != null && ((!Game1.player.CurrentTool.Name.Equals("Seeds") || Game1.pickingTool) && (Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), (int)Game1.player.Position.Y - 38), Game1.viewport.Size) != null && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), Game1.player.getStandingY()), Game1.viewport.Size) == null))) Game1.drawTool(Game1.player); if (Game1.currentLocation.Map.GetLayer("AlwaysFront") != null) @@ -1329,17 +1315,16 @@ namespace StardewModdingAPI.Framework { int num4 = num3; viewport = Game1.graphics.GraphicsDevice.Viewport; - int width1 = viewport.Width; - if (num4 < width1) + int width = viewport.Width; + if (num4 < width) { SpriteBatch spriteBatch = Game1.spriteBatch; Texture2D staminaRect = Game1.staminaRect; int x = num3; int y = (int)num2; - int width2 = 1; viewport = Game1.graphics.GraphicsDevice.Viewport; int height = viewport.Height; - Microsoft.Xna.Framework.Rectangle destinationRectangle = new Microsoft.Xna.Framework.Rectangle(x, y, width2, height); + Microsoft.Xna.Framework.Rectangle destinationRectangle = new Microsoft.Xna.Framework.Rectangle(x, y, 1, height); Color color = Color.Red * 0.5f; spriteBatch.Draw(staminaRect, destinationRectangle, color); num3 += 64; @@ -1352,8 +1337,8 @@ namespace StardewModdingAPI.Framework { double num4 = (double)num5; viewport = Game1.graphics.GraphicsDevice.Viewport; - double height1 = (double)viewport.Height; - if (num4 < height1) + double height = (double)viewport.Height; + if (num4 < height) { SpriteBatch spriteBatch = Game1.spriteBatch; Texture2D staminaRect = Game1.staminaRect; @@ -1361,8 +1346,7 @@ namespace StardewModdingAPI.Framework int y = (int)num5; viewport = Game1.graphics.GraphicsDevice.Viewport; int width = viewport.Width; - int height2 = 1; - Microsoft.Xna.Framework.Rectangle destinationRectangle = new Microsoft.Xna.Framework.Rectangle(x, y, width, height2); + Microsoft.Xna.Framework.Rectangle destinationRectangle = new Microsoft.Xna.Framework.Rectangle(x, y, width, 1); Color color = Color.Red * 0.5f; spriteBatch.Draw(staminaRect, destinationRectangle, color); num5 += 64f; @@ -1379,9 +1363,11 @@ namespace StardewModdingAPI.Framework this.drawHUD(); events.RenderedHud.RaiseEmpty(); } - else if (Game1.activeClickableMenu == null && Game1.farmEvent == null) - Game1.spriteBatch.Draw(Game1.mouseCursors, new Vector2((float)Game1.getOldMouseX(), (float)Game1.getOldMouseY()), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.mouseCursors, 0, 16, 16)), Color.White, 0.0f, Vector2.Zero, (float)(4.0 + (double)Game1.dialogueButtonScale / 150.0), SpriteEffects.None, 1f); - if (Game1.hudMessages.Count > 0 && (!Game1.eventUp || Game1.isFestival())) + else if (Game1.activeClickableMenu == null) + { + FarmEvent farmEvent = Game1.farmEvent; + } + if (Game1.hudMessages.Count > 0) { for (int i = Game1.hudMessages.Count - 1; i >= 0; --i) Game1.hudMessages[i].draw(Game1.spriteBatch, i); @@ -1393,26 +1379,8 @@ namespace StardewModdingAPI.Framework this.drawDialogueBox(); if (Game1.progressBar) { - SpriteBatch spriteBatch1 = Game1.spriteBatch; - Texture2D fadeToBlackRect = Game1.fadeToBlackRect; - int x1 = (Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Width - Game1.dialogueWidth) / 2; - rectangle = Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea(); - int y1 = rectangle.Bottom - 128; - int dialogueWidth = Game1.dialogueWidth; - int height1 = 32; - Microsoft.Xna.Framework.Rectangle destinationRectangle1 = new Microsoft.Xna.Framework.Rectangle(x1, y1, dialogueWidth, height1); - Color lightGray = Color.LightGray; - spriteBatch1.Draw(fadeToBlackRect, destinationRectangle1, lightGray); - SpriteBatch spriteBatch2 = Game1.spriteBatch; - Texture2D staminaRect = Game1.staminaRect; - int x2 = (Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Width - Game1.dialogueWidth) / 2; - rectangle = Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea(); - int y2 = rectangle.Bottom - 128; - int width = (int)((double)Game1.pauseAccumulator / (double)Game1.pauseTime * (double)Game1.dialogueWidth); - int height2 = 32; - Microsoft.Xna.Framework.Rectangle destinationRectangle2 = new Microsoft.Xna.Framework.Rectangle(x2, y2, width, height2); - Color dimGray = Color.DimGray; - spriteBatch2.Draw(staminaRect, destinationRectangle2, dimGray); + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle((Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Width - Game1.dialogueWidth) / 2, Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Bottom - 128, Game1.dialogueWidth, 32), Color.LightGray); + Game1.spriteBatch.Draw(Game1.staminaRect, new Microsoft.Xna.Framework.Rectangle((Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Width - Game1.dialogueWidth) / 2, Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Bottom - 128, (int)((double)Game1.pauseAccumulator / (double)Game1.pauseTime * (double)Game1.dialogueWidth), 32), Color.DimGray); } if (Game1.eventUp && Game1.currentLocation != null && Game1.currentLocation.currentEvent != null) Game1.currentLocation.currentEvent.drawAfterMap(Game1.spriteBatch); @@ -1497,6 +1465,8 @@ namespace StardewModdingAPI.Framework } else if (Game1.farmEvent != null) Game1.farmEvent.drawAboveEverything(Game1.spriteBatch); + if (Game1.emoteMenu != null) + Game1.emoteMenu.draw(Game1.spriteBatch); if (Game1.HostPaused) { string s = Game1.content.LoadString("Strings\\StringsFromCSFiles:DayTimeMoneyBox.cs.10378"); diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index a64dc89b..c4086712 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -152,12 +152,12 @@ namespace StardewModdingAPI.Metadata case "characters\\farmer\\farmer_base": // Farmer if (Game1.player == null || !Game1.player.IsMale) return false; - return Game1.player.FarmerRenderer = new FarmerRenderer(key); + return Game1.player.FarmerRenderer = new FarmerRenderer(key, Game1.player); case "characters\\farmer\\farmer_girl_base": // Farmer if (Game1.player == null || Game1.player.IsMale) return false; - return Game1.player.FarmerRenderer = new FarmerRenderer(key); + return Game1.player.FarmerRenderer = new FarmerRenderer(key, Game1.player); case "characters\\farmer\\hairstyles": // Game1.loadContent return FarmerRenderer.hairStylesTexture = content.Load(key); -- cgit From aa7e4b9c36bf14656493ee0a7ab2dab394a3cf64 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 18 Apr 2019 22:42:04 -0400 Subject: add new DLL to SMAPI references, support older game versions in package, fix name on Linux/Mac (#638) --- build/common.targets | 10 +++++++++- src/SMAPI.ModBuildConfig/build/smapi.targets | 10 +++++----- 2 files changed, 14 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/build/common.targets b/build/common.targets index 0781d568..4aac55fc 100644 --- a/build/common.targets +++ b/build/common.targets @@ -45,10 +45,14 @@ - + $(GamePath)\Stardew Valley.exe False + + $(GamePath)\StardewValley.GameData.dll + False + $(GamePath)\Netcode.dll False @@ -73,6 +77,10 @@ $(GamePath)\StardewValley.exe False + + $(GamePath)\StardewValley.GameData.MonoGame.dll + False + $(GamePath)\MonoGame.Framework.dll False diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets index e3211466..f703477f 100644 --- a/src/SMAPI.ModBuildConfig/build/smapi.targets +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -95,7 +95,7 @@ false true - + $(GamePath)\StardewValley.GameData.dll false true @@ -113,7 +113,7 @@ $(GamePath)\xTile.dll false - False + false true @@ -131,7 +131,7 @@ $(GamePath)\MonoGame.Framework.dll false - False + false true @@ -139,8 +139,8 @@ false true - - $(GamePath)\StardewValley.GameData.dll + + $(GamePath)\StardewValley.GameData.MonoGame.dll false true -- cgit From b887ecb30b4c41d58c6211ea00423f8259eebef5 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 18 Apr 2019 22:46:00 -0400 Subject: fix not applied to files matched by convention, update readme --- docs/mod-build-config.md | 45 ++++++---- .../Framework/ModFileManager.cs | 97 ++++++++++++---------- src/SMAPI.ModBuildConfig/package.nuspec | 1 + 3 files changed, 83 insertions(+), 60 deletions(-) (limited to 'src') diff --git a/docs/mod-build-config.md b/docs/mod-build-config.md index 8bd7b79d..4f46100a 100644 --- a/docs/mod-build-config.md +++ b/docs/mod-build-config.md @@ -26,16 +26,9 @@ The package... ## Configure ### Deploy files into the `Mods` folder -Your mod is copied into the game's `Mods` folder (with a subfolder matching your project name) -when you rebuild the code. The package automatically includes... - -* any build output; -* your `manifest.json`; -* any [`i18n` files](https://stardewvalleywiki.com/Modding:Translations); -* the `assets` folder if present. - -To add custom files to the mod folder, just [add them to the build output](https://stackoverflow.com/a/10828462/262123). -(If your project references another mod, make sure the reference is [_not_ marked 'copy local'](https://msdn.microsoft.com/en-us/library/t1zz5y8c(v=vs.100).aspx).) +Your mod is copied into the game's `Mods` folder when you rebuild the code, with a subfolder +matching the mod's project name. This includes the files set via [_Files included in the release_](#files-included-in-release) +below. You can change the mod's folder name by adding this above the first `` in your `.csproj`: @@ -49,9 +42,9 @@ If you don't want to deploy the mod automatically, you can add this: ``` ### Create release zip -A zip file is also created in the build output folder when you rebuild the code. This contains the -same files deployed to the `Mods` folder, in the recommended format for uploading to Nexus Mods or -other sites. +A zip file is also created in the build output folder when you rebuild the code. This includes the +files set via [_Files included in the release_](#files-included-in-release) below, in the format +recommended for uploading to Nexus Mods or other sites. You can change the zipped folder name (and zip name) by adding this above the first `` in your `.csproj`: @@ -118,15 +111,30 @@ or you have multiple installs, you can specify the path yourself. There's two wa The configuration will check your custom path first, then fall back to the default paths (so it'll still compile on a different computer). -### Ignore files -If you don't want to include a file in the mod folder or release zip: -* Make sure it's not copied to the build output. For a DLL, make sure the reference is [not marked 'copy local'](https://msdn.microsoft.com/en-us/library/t1zz5y8c(v=vs.100).aspx). +You access the game path via `$(GamePath)` in MSBuild properties, if you need to reference another +file in the game folder. + +### Files included in release +The package automatically considers these files to be part of your mod: + +* the `manifest.json` in your project; +* the [`i18n` files](https://stardewvalleywiki.com/Modding:Translations) in your project (if any); +* the `assets` folder in your project (if present); +* and any files in the build output folder. + +To add custom files to the release, just [add them to the build output](https://stackoverflow.com/a/10828462/262123). +(If your project references another mod, make sure the reference is [_not_ marked 'copy local'](https://msdn.microsoft.com/en-us/library/t1zz5y8c(v=vs.100).aspx).) + +To exclude a file from the release: +* Make sure it's not copied to the build output. (For a DLL, make sure the reference is [not marked 'copy local'](https://msdn.microsoft.com/en-us/library/t1zz5y8c(v=vs.100).aspx).) This doesn't apply to `manifest.json`, + `assets`, or `i18n` which are copied regardless.) * Or add this to your `.csproj` file under the `\.txt$, \.pdf$ ``` This is a comma-delimited list of regular expression patterns. If any pattern matches a file's - relative path in your mod folder, that file won't be included. + relative path in your mod folder, that file won't be included. (This also works for `assets` and + `i18n`.) ### Non-mod projects You can use the package in non-mod projects too (e.g. unit tests or framework DLLs). You'll need to @@ -216,8 +224,9 @@ _[Game path](#game-path)_ above. ### Upcoming release * Updated for Stardew Valley 1.4. * If the project contains an `assets` folder, its contents are now included in the mod automatically. -* Dropped support for very old versions of SMAPI and Visual Studio. * Fixed `Newtonsoft.Json.pdb` included in release zips when Json.NET is referenced directly. +* Fixed `` not working for `i18n` files. +* Dropped support for very old versions of SMAPI and Visual Studio. ### 2.2 * Added support for SMAPI 2.8+ (still compatible with earlier versions). diff --git a/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs b/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs index 6c11b0fe..e67a18c1 100644 --- a/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs +++ b/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs @@ -41,13 +41,63 @@ namespace StardewModdingAPI.ModBuildConfig.Framework if (!Directory.Exists(targetDir)) throw new UserErrorException("Could not create mod package because no build output was found."); + // collect files + foreach (Tuple entry in this.GetPossibleFiles(projectDir, targetDir)) + { + string relativePath = entry.Item1; + FileInfo file = entry.Item2; + + if (!this.ShouldIgnore(file, relativePath, ignoreFilePatterns)) + this.Files[relativePath] = file; + } + + // check for required files + if (validateRequiredModFiles) + { + // manifest + if (!this.Files.ContainsKey(this.ManifestFileName)) + throw new UserErrorException($"Could not create mod package because no {this.ManifestFileName} was found in the project or build output."); + + // DLL + // ReSharper disable once SimplifyLinqExpression + if (!this.Files.Any(p => !p.Key.EndsWith(".dll"))) + throw new UserErrorException("Could not create mod package because no .dll file was found in the project or build output."); + } + } + + /// Get the files in the mod package. + public IDictionary GetFiles() + { + return new Dictionary(this.Files, StringComparer.InvariantCultureIgnoreCase); + } + + /// Get a semantic version from the mod manifest. + /// The manifest is missing or invalid. + public string GetManifestVersion() + { + 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 + + return manifest.Version.ToString(); + } + + + /********* + ** Private methods + *********/ + /// Get all files to include in the mod folder, not accounting for ignore patterns. + /// The folder containing the project files. + /// The folder containing the build output. + /// Returns tuples containing the relative path within the mod folder, and the file to copy to it. + private IEnumerable> GetPossibleFiles(string projectDir, string targetDir) + { // project manifest bool hasProjectManifest = false; { - FileInfo manifest = new FileInfo(Path.Combine(projectDir, "manifest.json")); + FileInfo manifest = new FileInfo(Path.Combine(projectDir, this.ManifestFileName)); if (manifest.Exists) { - this.Files[this.ManifestFileName] = manifest; + yield return Tuple.Create(this.ManifestFileName, manifest); hasProjectManifest = true; } } @@ -58,7 +108,7 @@ namespace StardewModdingAPI.ModBuildConfig.Framework if (translationsFolder.Exists) { foreach (FileInfo file in translationsFolder.EnumerateFiles()) - this.Files[Path.Combine("i18n", file.Name)] = file; + yield return Tuple.Create(Path.Combine("i18n", file.Name), file); hasProjectTranslations = true; } @@ -70,7 +120,7 @@ namespace StardewModdingAPI.ModBuildConfig.Framework foreach (FileInfo file in assetsFolder.EnumerateFiles("*", SearchOption.AllDirectories)) { string relativePath = PathUtilities.GetRelativePath(projectDir, file.FullName); - this.Files[relativePath] = file; + yield return Tuple.Create(relativePath, file); } hasAssetsFolder = true; } @@ -91,48 +141,11 @@ namespace StardewModdingAPI.ModBuildConfig.Framework if (hasAssetsFolder && this.EqualsInvariant(segments[0], "assets")) continue; - // handle ignored files - if (this.ShouldIgnore(file, relativePath, ignoreFilePatterns)) - continue; - // add file - this.Files[relativePath] = file; - } - - // check for required files - if (validateRequiredModFiles) - { - // manifest - if (!this.Files.ContainsKey(this.ManifestFileName)) - throw new UserErrorException($"Could not create mod package because no {this.ManifestFileName} was found in the project or build output."); - - // DLL - // ReSharper disable once SimplifyLinqExpression - if (!this.Files.Any(p => !p.Key.EndsWith(".dll"))) - throw new UserErrorException("Could not create mod package because no .dll file was found in the project or build output."); + yield return Tuple.Create(relativePath, file); } } - /// Get the files in the mod package. - public IDictionary GetFiles() - { - return new Dictionary(this.Files, StringComparer.InvariantCultureIgnoreCase); - } - - /// Get a semantic version from the mod manifest. - /// The manifest is missing or invalid. - public string GetManifestVersion() - { - 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 - - return manifest.Version.ToString(); - } - - - /********* - ** Private methods - *********/ /// Get whether a build output file should be ignored. /// The file to check. /// The file's relative path in the package. diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec index b6ef98f0..79a4f008 100644 --- a/src/SMAPI.ModBuildConfig/package.nuspec +++ b/src/SMAPI.ModBuildConfig/package.nuspec @@ -17,6 +17,7 @@ - If the project contains an `assets` folder, its contents are now included in the mod automatically. - Dropped support for very old versions of SMAPI and Visual Studio. - Fixed `Newtonsoft.Json.pdb` included in release zips when Json.NET is referenced directly. + - Fixed `<IgnoreModFilePatterns>` not working for `i18n` files. -- cgit From 6f63bd00248fd90649718a1a77926a48f2df0e51 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 21 Apr 2019 13:23:33 -0400 Subject: update nuspec for alpha release --- src/SMAPI.ModBuildConfig/Properties/AssemblyInfo.cs | 4 ++-- src/SMAPI.ModBuildConfig/package.nuspec | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/SMAPI.ModBuildConfig/Properties/AssemblyInfo.cs b/src/SMAPI.ModBuildConfig/Properties/AssemblyInfo.cs index e051bfbd..255ce509 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.2.0")] -[assembly: AssemblyFileVersion("2.2.0")] +[assembly: AssemblyVersion("3.0.0")] +[assembly: AssemblyFileVersion("3.0.0")] diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec index 79a4f008..f66a3504 100644 --- a/src/SMAPI.ModBuildConfig/package.nuspec +++ b/src/SMAPI.ModBuildConfig/package.nuspec @@ -2,12 +2,13 @@ Pathoschild.Stardew.ModBuildConfig - 3.0.0 + 3.0.0-alpha.20190419 Build package for SMAPI mods Pathoschild Pathoschild false - https://github.com/Pathoschild/SMAPI/blob/develop/LICENSE.txt + MIT + https://github.com/Pathoschild/SMAPI/blob/develop/docs/mod-build-config.md#readme https://raw.githubusercontent.com/Pathoschild/SMAPI/develop/src/SMAPI.ModBuildConfig/assets/nuget-icon.png Automates the build configuration for crossplatform Stardew Valley SMAPI mods. For SMAPI 2.11 or later. -- cgit From 99a4b2a3b90f4abbc7bfbe2020bf186480dce05f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 21 Apr 2019 17:40:48 -0400 Subject: update compatibility list, rm SMAPI 3.0 entries for mods which are incompatible with SDV 1.4 (#638) --- .../wwwroot/StardewModdingAPI.metadata.json | 78 +++------------------- 1 file changed, 8 insertions(+), 70 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json b/src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json index a3336ed3..f1e0023e 100644 --- a/src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json +++ b/src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json @@ -234,6 +234,14 @@ "~ | StatusReasonPhrase": "debug mode was removed in SMAPI 1.0." }, + /********* + ** Broke in SDV 1.4 + *********/ + "Loved Labels": { + "ID": "Advize.LovedLabels", + "~2.2.1-unofficial.2-pathoschild | Status": "AssumeBroken" // runtime reflection errors + }, + /********* ** Broke in SMAPI 3.0 (runtime errors due to lifecycle changes) *********/ @@ -257,22 +265,12 @@ "~2.3.1-unofficial.7-pathoschild | Status": "AssumeBroken" }, - "Companion NPCs": { - "ID": "Redwood.CompanionNPCs", - "~0.0.9 | Status": "AssumeBroken" - }, - "Content Patcher": { "ID": "Pathoschild.ContentPatcher", "Default | UpdateKey": "Nexus:1915", "~1.6.4 | Status": "AssumeBroken" }, - "Crop Transplant Mod": { - "ID": "DIGUS.CropTransplantMod", - "~1.1.3 | Status": "AssumeBroken" - }, - "Custom Adventure Guild Challenges": { "ID": "DefenTheNation.CustomGuildChallenges", "~1.8 | Status": "AssumeBroken" @@ -284,36 +282,6 @@ "~2.10.10 | Status": "AssumeBroken" // possibly due to PyTK }, - "Deep Woods": { - "ID": "maxvollmer.deepwoodsmod", - "~1.5-beta.1 | Status": "AssumeBroken" - }, - - "Everlasting Baits and Unbreaking Tackles": { - "ID": "DIGUS.EverlastingBaitsAndUnbreakableTacklesMod", - "~1.2.4 | Status": "AssumeBroken" - }, - - "Farmhouse Redone": { - "ID": "mabelsyrup.farmhouse", - "~0.2 | Status": "AssumeBroken" - }, - - "Geode Info Menu": { - "ID": "cat.geodeinfomenu", - "~1.5 | Status": "AssumeBroken" - }, - - "Harp of Yoba Redux": { - "ID": "Platonymous.HarpOfYobaRedux", - "~2.6.3 | Status": "AssumeBroken" // possibly due to PyTK - }, - - "Infested Levels": { - "ID": "Eireon.InfestedLevels", - "~1.0.5 | Status": "AssumeBroken" - }, - "JoJaBan - Arcade Sokoban": { "ID": "Platonymous.JoJaBan", "~0.4.3 | Status": "AssumeBroken" // possibly due to PyTK @@ -329,16 +297,6 @@ "~1.4 | Status": "AssumeBroken" }, - "Mushroom Levels": { - "ID": "Eireon.MushroomLevels", - "~1.0.4 | Status": "AssumeBroken" - }, - - "Notes": { - "ID": "Platonymous.Notes", - "~1.0.5 | Status": "AssumeBroken" // possibly due to PyTK - }, - "Quick Start": { "ID": "WuestMan.QuickStart", "~1.5 | Status": "AssumeBroken" @@ -349,26 +307,6 @@ "~1.2.7 | Status": "AssumeBroken" // possibly due to PyTK }, - "Separate Money": { - "ID": "funnysnek.SeparateMoney", - "~1.4.2 | Status": "AssumeBroken" - }, - - "Split Money": { - "ID": "Platonymous.SplitMoney", - "~1.2.4 | Status": "AssumeBroken" // possibly due to PyTK - }, - - "Stack to Nearby Chests": { - "ID": "Ilyaki.StackToNearbyChests", - "~1.4.4 | Status": "AssumeBroken" - }, - - "Tree Transplant": { - "ID": "TreeTransplant", - "~1.0.5 | Status": "AssumeBroken" - }, - /********* ** Broke in SDV 1.3.36 *********/ -- cgit From f8e32f4433647b7bd69590b937927808168649df Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 24 Apr 2019 01:26:15 -0400 Subject: update release notes, tweak launch script comments (#640) --- docs/release-notes.md | 1 + src/SMAPI.Installer/unix-launcher.sh | 12 +++++------- 2 files changed, 6 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 32f4db37..12fd5a3d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -6,6 +6,7 @@ These changes have not been released yet. * Updated for Stardew Valley 1.4. * Improved performance. * Updated mod compatibility list. + * Rewrote launch script on Linux to improve compatibility (thanks to kurumushi and toastal!). * Fixed Save Backup not pruning old backups if they're uncompressed. * Fixed issues when a farmhand reconnects before the game notices they're disconnected. * Fixed 'received message' logs shown in non-developer mode. diff --git a/src/SMAPI.Installer/unix-launcher.sh b/src/SMAPI.Installer/unix-launcher.sh index fc08577f..0ca5c852 100644 --- a/src/SMAPI.Installer/unix-launcher.sh +++ b/src/SMAPI.Installer/unix-launcher.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # MonoKickstart Shell Script # Written by Ethan "flibitijibibo" Lee -# Modified for StardewModdingAPI by Viz and Pathoschild +# Modified for SMAPI by various contributors # Move to script's directory cd "$(dirname "$0")" || exit $? @@ -61,9 +61,7 @@ else COMMAND="type" fi - # open SMAPI in terminal - # First let's try xterm (best compatiblity) or find a sensible default - # Setting TERMINAL should let you override this at the commandline for easier testing of other terminals. + # select terminal (prefer $TERMINAL for overrides and testing, then xterm for best compatibility, then known supported terminals) for terminal in "$TERMINAL" xterm x-terminal-emulator kitty terminator xfce4-terminal gnome-terminal konsole terminal termite; do if $COMMAND "$terminal" 2>/dev/null; then # Find the true shell behind x-terminal-emulator @@ -72,15 +70,15 @@ else break; else export LAUNCHTERM="$(basename "$(readlink -ef which x-terminal-emulator)")" - # Remember that we are using x-terminal-emulator just in case it points outside the $PATH + # Remember that we're using x-terminal-emulator just in case it points outside the $PATH export XTE=1 break; fi fi done + # if no terminal was found, run in current shell or with no output if [ -z "$LAUNCHTERM" ]; then - # We failed to detect a terminal, run in current shell, or with no output sh -c 'TERM=xterm $LAUNCHER' if [ $? -eq 127 ]; then $LAUNCHER --no-terminal @@ -88,7 +86,7 @@ else exit fi - # Now let's run the terminal and account for quirks, or if no terminal was found, run in the current process. + # run in selected terminal and account for quirks case $LAUNCHTERM in terminator) # Terminator converts -e to -x when used through x-terminal-emulator for some reason -- cgit From abffdc2dab2a1c03904427b251acac9d800e0912 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 24 Apr 2019 23:45:34 -0400 Subject: simplify project names --- build/common.targets | 20 +-- build/prepare-install-package.targets | 12 +- src/SMAPI.Installer/SMAPI.Installer.csproj | 45 ++++++ .../StardewModdingAPI.Installer.csproj | 45 ------ src/SMAPI.Internal/SMAPI.Internal.shproj | 13 ++ .../StardewModdingAPI.Internal.shproj | 13 -- .../SMAPI.ModBuildConfig.Analyzer.Tests.csproj | 2 +- .../SMAPI.ModBuildConfig.Analyzer.csproj | 24 +++ ...tardewModdingAPI.ModBuildConfig.Analyzer.csproj | 24 --- .../SMAPI.ModBuildConfig.csproj | 40 +++++ .../StardewModdingAPI.ModBuildConfig.csproj | 40 ----- .../SMAPI.Mods.ConsoleCommands.csproj | 35 +++++ .../StardewModdingAPI.Mods.ConsoleCommands.csproj | 35 ----- .../SMAPI.Mods.SaveBackup.csproj | 35 +++++ .../StardewModdingAPI.Mods.SaveBackup.csproj | 35 ----- src/SMAPI.Tests/SMAPI.Tests.csproj | 40 +++++ src/SMAPI.Tests/StardewModdingAPI.Tests.csproj | 40 ----- .../SMAPI.Toolkit.CoreInterfaces.csproj | 18 +++ ...StardewModdingAPI.Toolkit.CoreInterfaces.csproj | 18 --- src/SMAPI.Toolkit/SMAPI.Toolkit.csproj | 28 ++++ src/SMAPI.Toolkit/StardewModdingAPI.Toolkit.csproj | 28 ---- src/SMAPI.Web/SMAPI.Web.csproj | 46 ++++++ src/SMAPI.Web/StardewModdingAPI.Web.csproj | 46 ------ src/SMAPI.sln | 171 +++++++++++---------- src/SMAPI/SMAPI.csproj | 60 ++++++++ src/SMAPI/StardewModdingAPI.csproj | 60 -------- 26 files changed, 487 insertions(+), 486 deletions(-) create mode 100644 src/SMAPI.Installer/SMAPI.Installer.csproj delete mode 100644 src/SMAPI.Installer/StardewModdingAPI.Installer.csproj create mode 100644 src/SMAPI.Internal/SMAPI.Internal.shproj delete mode 100644 src/SMAPI.Internal/StardewModdingAPI.Internal.shproj create mode 100644 src/SMAPI.ModBuildConfig.Analyzer/SMAPI.ModBuildConfig.Analyzer.csproj delete mode 100644 src/SMAPI.ModBuildConfig.Analyzer/StardewModdingAPI.ModBuildConfig.Analyzer.csproj create mode 100644 src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj delete mode 100644 src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj create mode 100644 src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj delete mode 100644 src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj create mode 100644 src/SMAPI.Mods.SaveBackup/SMAPI.Mods.SaveBackup.csproj delete mode 100644 src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj create mode 100644 src/SMAPI.Tests/SMAPI.Tests.csproj delete mode 100644 src/SMAPI.Tests/StardewModdingAPI.Tests.csproj create mode 100644 src/SMAPI.Toolkit.CoreInterfaces/SMAPI.Toolkit.CoreInterfaces.csproj delete mode 100644 src/SMAPI.Toolkit.CoreInterfaces/StardewModdingAPI.Toolkit.CoreInterfaces.csproj create mode 100644 src/SMAPI.Toolkit/SMAPI.Toolkit.csproj delete mode 100644 src/SMAPI.Toolkit/StardewModdingAPI.Toolkit.csproj create mode 100644 src/SMAPI.Web/SMAPI.Web.csproj delete mode 100644 src/SMAPI.Web/StardewModdingAPI.Web.csproj create mode 100644 src/SMAPI/SMAPI.csproj delete mode 100644 src/SMAPI/StardewModdingAPI.csproj (limited to 'src') diff --git a/build/common.targets b/build/common.targets index 4aac55fc..9922bf15 100644 --- a/build/common.targets +++ b/build/common.targets @@ -32,12 +32,12 @@ - + - + @@ -47,15 +47,15 @@ $(GamePath)\Stardew Valley.exe - False + False $(GamePath)\StardewValley.GameData.dll - False + False $(GamePath)\Netcode.dll - False + False False @@ -79,7 +79,7 @@ $(GamePath)\StardewValley.GameData.MonoGame.dll - False + False $(GamePath)\MonoGame.Framework.dll @@ -114,7 +114,7 @@ - + @@ -124,17 +124,17 @@ - + - + - + diff --git a/build/prepare-install-package.targets b/build/prepare-install-package.targets index b7b70ed0..928191a2 100644 --- a/build/prepare-install-package.targets +++ b/build/prepare-install-package.targets @@ -40,12 +40,12 @@ - - - - - - + + + + + + diff --git a/src/SMAPI.Installer/SMAPI.Installer.csproj b/src/SMAPI.Installer/SMAPI.Installer.csproj new file mode 100644 index 00000000..a2e115e6 --- /dev/null +++ b/src/SMAPI.Installer/SMAPI.Installer.csproj @@ -0,0 +1,45 @@ + + + + SMAPI.Installer + SMAPI.Installer + net45 + false + latest + Exe + x86 + $(SolutionDir)\..\bin\$(Configuration)\Installer + false + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + + + + + diff --git a/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj b/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj deleted file mode 100644 index ac64a774..00000000 --- a/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj +++ /dev/null @@ -1,45 +0,0 @@ - - - - StardewModdingAPI.Installer - StardewModdingAPI.Installer - net45 - false - latest - Exe - x86 - $(SolutionDir)\..\bin\$(Configuration)\Installer - false - - - - - - - - - - - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - - - - - - diff --git a/src/SMAPI.Internal/SMAPI.Internal.shproj b/src/SMAPI.Internal/SMAPI.Internal.shproj new file mode 100644 index 00000000..a098828a --- /dev/null +++ b/src/SMAPI.Internal/SMAPI.Internal.shproj @@ -0,0 +1,13 @@ + + + + 85208f8d-6fd1-4531-be05-7142490f59fe + 14.0 + + + + + + + + diff --git a/src/SMAPI.Internal/StardewModdingAPI.Internal.shproj b/src/SMAPI.Internal/StardewModdingAPI.Internal.shproj deleted file mode 100644 index a098828a..00000000 --- a/src/SMAPI.Internal/StardewModdingAPI.Internal.shproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - 85208f8d-6fd1-4531-be05-7142490f59fe - 14.0 - - - - - - - - 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 747a1d69..44e7ffab 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj +++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj @@ -13,7 +13,7 @@ - + diff --git a/src/SMAPI.ModBuildConfig.Analyzer/SMAPI.ModBuildConfig.Analyzer.csproj b/src/SMAPI.ModBuildConfig.Analyzer/SMAPI.ModBuildConfig.Analyzer.csproj new file mode 100644 index 00000000..1d8d7227 --- /dev/null +++ b/src/SMAPI.ModBuildConfig.Analyzer/SMAPI.ModBuildConfig.Analyzer.csproj @@ -0,0 +1,24 @@ + + + + netstandard1.3 + false + false + bin + latest + + + + + + + + + + + + + + + + diff --git a/src/SMAPI.ModBuildConfig.Analyzer/StardewModdingAPI.ModBuildConfig.Analyzer.csproj b/src/SMAPI.ModBuildConfig.Analyzer/StardewModdingAPI.ModBuildConfig.Analyzer.csproj deleted file mode 100644 index 1d8d7227..00000000 --- a/src/SMAPI.ModBuildConfig.Analyzer/StardewModdingAPI.ModBuildConfig.Analyzer.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - - netstandard1.3 - false - false - bin - latest - - - - - - - - - - - - - - - - diff --git a/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj b/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj new file mode 100644 index 00000000..f39a1555 --- /dev/null +++ b/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj @@ -0,0 +1,40 @@ + + + + SMAPI.ModBuildConfig + SMAPI.ModBuildConfig + net45 + false + latest + x86 + false + + + + + + + + + mod-build-config.md + + + + + + + + + + + + + + mod-build-config.md + + + + + + + diff --git a/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj b/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj deleted file mode 100644 index 870973a0..00000000 --- a/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj +++ /dev/null @@ -1,40 +0,0 @@ - - - - StardewModdingAPI.ModBuildConfig - StardewModdingAPI.ModBuildConfig - net45 - false - latest - x86 - false - - - - - - - - - mod-build-config.md - - - - - - - - - - - - - - mod-build-config.md - - - - - - - diff --git a/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj b/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj new file mode 100644 index 00000000..13f92443 --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj @@ -0,0 +1,35 @@ + + + + SMAPI.Mods.ConsoleCommands + ConsoleCommands + net45 + false + latest + $(SolutionDir)\..\bin\$(Configuration)\Mods\ConsoleCommands + false + x86 + + + + + False + + + + + + Properties\GlobalAssemblyInfo.cs + + + + + + PreserveNewest + + + + + + + diff --git a/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj b/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj deleted file mode 100644 index b535e2fd..00000000 --- a/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj +++ /dev/null @@ -1,35 +0,0 @@ - - - - StardewModdingAPI.Mods.ConsoleCommands - ConsoleCommands - net45 - false - latest - $(SolutionDir)\..\bin\$(Configuration)\Mods\ConsoleCommands - false - x86 - - - - - False - - - - - - Properties\GlobalAssemblyInfo.cs - - - - - - PreserveNewest - - - - - - - diff --git a/src/SMAPI.Mods.SaveBackup/SMAPI.Mods.SaveBackup.csproj b/src/SMAPI.Mods.SaveBackup/SMAPI.Mods.SaveBackup.csproj new file mode 100644 index 00000000..9375cb64 --- /dev/null +++ b/src/SMAPI.Mods.SaveBackup/SMAPI.Mods.SaveBackup.csproj @@ -0,0 +1,35 @@ + + + + SMAPI.Mods.SaveBackup + SaveBackup + net45 + false + latest + $(SolutionDir)\..\bin\$(Configuration)\Mods\SaveBackup + false + x86 + + + + + False + + + + + + Properties\GlobalAssemblyInfo.cs + + + + + + PreserveNewest + + + + + + + diff --git a/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj b/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj deleted file mode 100644 index 460f3c93..00000000 --- a/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj +++ /dev/null @@ -1,35 +0,0 @@ - - - - StardewModdingAPI.Mods.SaveBackup - SaveBackup - net45 - false - latest - $(SolutionDir)\..\bin\$(Configuration)\Mods\SaveBackup - false - x86 - - - - - False - - - - - - Properties\GlobalAssemblyInfo.cs - - - - - - PreserveNewest - - - - - - - diff --git a/src/SMAPI.Tests/SMAPI.Tests.csproj b/src/SMAPI.Tests/SMAPI.Tests.csproj new file mode 100644 index 00000000..1c414127 --- /dev/null +++ b/src/SMAPI.Tests/SMAPI.Tests.csproj @@ -0,0 +1,40 @@ + + + + SMAPI.Tests + SMAPI.Tests + net45 + false + latest + x86 + + + + + + + + + + + + + + + + + + + + + Properties\GlobalAssemblyInfo.cs + + + + + + + + + + diff --git a/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj b/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj deleted file mode 100644 index 68cf7b34..00000000 --- a/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj +++ /dev/null @@ -1,40 +0,0 @@ - - - - StardewModdingAPI.Tests - StardewModdingAPI.Tests - net45 - false - latest - x86 - - - - - - - - - - - - - - - - - - - - - Properties\GlobalAssemblyInfo.cs - - - - - - - - - - diff --git a/src/SMAPI.Toolkit.CoreInterfaces/SMAPI.Toolkit.CoreInterfaces.csproj b/src/SMAPI.Toolkit.CoreInterfaces/SMAPI.Toolkit.CoreInterfaces.csproj new file mode 100644 index 00000000..7d8fc998 --- /dev/null +++ b/src/SMAPI.Toolkit.CoreInterfaces/SMAPI.Toolkit.CoreInterfaces.csproj @@ -0,0 +1,18 @@ + + + + net4.5;netstandard2.0 + SMAPI + false + ..\..\bin\$(Configuration)\SMAPI.Toolkit.CoreInterfaces + ..\..\bin\$(Configuration)\SMAPI.Toolkit.CoreInterfaces\$(TargetFramework)\SMAPI.Toolkit.CoreInterfaces.xml + latest + + + + + + + + + diff --git a/src/SMAPI.Toolkit.CoreInterfaces/StardewModdingAPI.Toolkit.CoreInterfaces.csproj b/src/SMAPI.Toolkit.CoreInterfaces/StardewModdingAPI.Toolkit.CoreInterfaces.csproj deleted file mode 100644 index cbbb7fc9..00000000 --- a/src/SMAPI.Toolkit.CoreInterfaces/StardewModdingAPI.Toolkit.CoreInterfaces.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - net4.5;netstandard2.0 - StardewModdingAPI - false - ..\..\bin\$(Configuration)\SMAPI.Toolkit.CoreInterfaces - ..\..\bin\$(Configuration)\SMAPI.Toolkit.CoreInterfaces\$(TargetFramework)\StardewModdingAPI.Toolkit.CoreInterfaces.xml - latest - - - - - - - - - diff --git a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj new file mode 100644 index 00000000..8a029e62 --- /dev/null +++ b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj @@ -0,0 +1,28 @@ + + + + net4.5;netstandard2.0 + false + ..\..\bin\$(Configuration)\SMAPI.Toolkit + ..\..\bin\$(Configuration)\SMAPI.Toolkit\$(TargetFramework)\SMAPI.Toolkit.xml + latest + x86 + + + + + + + + + + + + + + + + + + + diff --git a/src/SMAPI.Toolkit/StardewModdingAPI.Toolkit.csproj b/src/SMAPI.Toolkit/StardewModdingAPI.Toolkit.csproj deleted file mode 100644 index a386fc5e..00000000 --- a/src/SMAPI.Toolkit/StardewModdingAPI.Toolkit.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - - net4.5;netstandard2.0 - false - ..\..\bin\$(Configuration)\SMAPI.Toolkit - ..\..\bin\$(Configuration)\SMAPI.Toolkit\$(TargetFramework)\StardewModdingAPI.Toolkit.xml - latest - x86 - - - - - - - - - - - - - - - - - - - diff --git a/src/SMAPI.Web/SMAPI.Web.csproj b/src/SMAPI.Web/SMAPI.Web.csproj new file mode 100644 index 00000000..023a9333 --- /dev/null +++ b/src/SMAPI.Web/SMAPI.Web.csproj @@ -0,0 +1,46 @@ + + + + netcoreapp2.0 + false + latest + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $(IncludeRazorContentInPack) + + + $(IncludeRazorContentInPack) + + + PreserveNewest + + + + diff --git a/src/SMAPI.Web/StardewModdingAPI.Web.csproj b/src/SMAPI.Web/StardewModdingAPI.Web.csproj deleted file mode 100644 index 88a13f7b..00000000 --- a/src/SMAPI.Web/StardewModdingAPI.Web.csproj +++ /dev/null @@ -1,46 +0,0 @@ - - - - netcoreapp2.0 - false - latest - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - $(IncludeRazorContentInPack) - - - $(IncludeRazorContentInPack) - - - PreserveNewest - - - - diff --git a/src/SMAPI.sln b/src/SMAPI.sln index ffd50455..0a2d1511 100644 --- a/src/SMAPI.sln +++ b/src/SMAPI.sln @@ -3,8 +3,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.28729.10 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI", "SMAPI\StardewModdingAPI.csproj", "{1298F2B2-57BD-4647-AF70-1FCBBEE500B6}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".root", ".root", "{86C452BE-D2D8-45B4-B63F-E329EB06CEDA}" ProjectSection(SolutionItems) = preProject ..\.editorconfig = ..\.editorconfig @@ -13,25 +11,17 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".root", ".root", "{86C452BE ..\LICENSE.txt = ..\LICENSE.txt EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StardewModdingAPI.Installer", "SMAPI.Installer\StardewModdingAPI.Installer.csproj", "{0ED5EAD8-5D85-420D-8101-6D8CCCE29C9B}" - ProjectSection(ProjectDependencies) = postProject - {E4113F3E-3CAA-4288-9378-39A77BA625DB} = {E4113F3E-3CAA-4288-9378-39A77BA625DB} - {8C2CA4AB-BA8A-446A-B59E-9D6502E145F7} = {8C2CA4AB-BA8A-446A-B59E-9D6502E145F7} - {1298F2B2-57BD-4647-AF70-1FCBBEE500B6} = {1298F2B2-57BD-4647-AF70-1FCBBEE500B6} +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{4B1CEB70-F756-4A57-AAE8-8CD78C475F25}" + ProjectSection(SolutionItems) = preProject + ..\.github\CONTRIBUTING.md = ..\.github\CONTRIBUTING.md + ..\.github\SUPPORT.md = ..\.github\SUPPORT.md EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StardewModdingAPI.Tests", "SMAPI.Tests\StardewModdingAPI.Tests.csproj", "{E023DA12-5960-4101-80B9-A7DCE955725C}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StardewModdingAPI.Web", "SMAPI.Web\StardewModdingAPI.Web.csproj", "{A308F679-51A3-4006-92D5-BAEC7EBD01A1}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Internal", "Internal", "{82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{EB35A917-67B9-4EFA-8DFC-4FB49B3949BB}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ISSUE_TEMPLATE", "ISSUE_TEMPLATE", "{F4453AB6-D7D6-447F-A973-956CC777968F}" ProjectSection(SolutionItems) = preProject - ..\docs\mod-build-config.md = ..\docs\mod-build-config.md - ..\docs\README.md = ..\docs\README.md - ..\docs\release-notes.md = ..\docs\release-notes.md - ..\docs\technical-docs.md = ..\docs\technical-docs.md + ..\.github\ISSUE_TEMPLATE\bug_report.md = ..\.github\ISSUE_TEMPLATE\bug_report.md + ..\.github\ISSUE_TEMPLATE\feature_request.md = ..\.github\ISSUE_TEMPLATE\feature_request.md + ..\.github\ISSUE_TEMPLATE\general.md = ..\.github\ISSUE_TEMPLATE\general.md EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{09CF91E5-5BAB-4650-A200-E5EA9A633046}" @@ -42,34 +32,44 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{09CF91E5 ..\build\prepare-nuget-package.targets = ..\build\prepare-nuget-package.targets EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StardewModdingAPI.ModBuildConfig", "SMAPI.ModBuildConfig\StardewModdingAPI.ModBuildConfig.csproj", "{C11D0AFB-2893-41A9-AD55-D002F032D6AD}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{EB35A917-67B9-4EFA-8DFC-4FB49B3949BB}" + ProjectSection(SolutionItems) = preProject + ..\docs\mod-build-config.md = ..\docs\mod-build-config.md + ..\docs\README.md = ..\docs\README.md + ..\docs\release-notes.md = ..\docs\release-notes.md + ..\docs\technical-docs.md = ..\docs\technical-docs.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Internal", "Internal", "{82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StardewModdingAPI.ModBuildConfig.Analyzer", "SMAPI.ModBuildConfig.Analyzer\StardewModdingAPI.ModBuildConfig.Analyzer.csproj", "{80AD8528-AA49-4731-B4A6-C691845815A1}" +Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "SMAPI.Internal", "SMAPI.Internal\SMAPI.Internal.shproj", "{85208F8D-6FD1-4531-BE05-7142490F59FE}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.ModBuildConfig.Analyzer.Tests", "SMAPI.ModBuildConfig.Analyzer.Tests\SMAPI.ModBuildConfig.Analyzer.Tests.csproj", "{0CF97929-B0D0-4D73-B7BF-4FF7191035F9}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.ModBuildConfig.Analyzer.Tests", "SMAPI.ModBuildConfig.Analyzer.Tests\SMAPI.ModBuildConfig.Analyzer.Tests.csproj", "{680B2641-81EA-467C-86A5-0E81CDC57ED0}" EndProject -Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "StardewModdingAPI.Internal", "SMAPI.Internal\StardewModdingAPI.Internal.shproj", "{85208F8D-6FD1-4531-BE05-7142490F59FE}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Tests", "SMAPI.Tests\SMAPI.Tests.csproj", "{AA95884B-7097-476E-92C8-D0500DE9D6D1}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StardewModdingAPI.Mods.ConsoleCommands", "SMAPI.Mods.ConsoleCommands\StardewModdingAPI.Mods.ConsoleCommands.csproj", "{8C2CA4AB-BA8A-446A-B59E-9D6502E145F7}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI", "SMAPI\SMAPI.csproj", "{E6DA2198-7686-4F1D-B312-4A4DC70884C0}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StardewModdingAPI.Mods.SaveBackup", "SMAPI.Mods.SaveBackup\StardewModdingAPI.Mods.SaveBackup.csproj", "{E4113F3E-3CAA-4288-9378-39A77BA625DB}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Installer", "SMAPI.Installer\SMAPI.Installer.csproj", "{0A9BB24F-15FF-4C26-B1A2-81F7AE316518}" + ProjectSection(ProjectDependencies) = postProject + {0634EA4C-3B8F-42DB-AEA6-CA9E4EF6E92F} = {0634EA4C-3B8F-42DB-AEA6-CA9E4EF6E92F} + {CD53AD6F-97F4-4872-A212-50C2A0FD3601} = {CD53AD6F-97F4-4872-A212-50C2A0FD3601} + {E6DA2198-7686-4F1D-B312-4A4DC70884C0} = {E6DA2198-7686-4F1D-B312-4A4DC70884C0} + EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StardewModdingAPI.Toolkit", "SMAPI.Toolkit\StardewModdingAPI.Toolkit.csproj", "{EA5CFD2E-9453-4D29-B80F-8E0EA23F4AC6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.ModBuildConfig", "SMAPI.ModBuildConfig\SMAPI.ModBuildConfig.csproj", "{1B3821E6-D030-402C-B3A1-7CA45C2800EA}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StardewModdingAPI.Toolkit.CoreInterfaces", "SMAPI.Toolkit.CoreInterfaces\StardewModdingAPI.Toolkit.CoreInterfaces.csproj", "{D5CFD923-37F1-4BC3-9BE8-E506E202AC28}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.ModBuildConfig.Analyzer", "SMAPI.ModBuildConfig.Analyzer\SMAPI.ModBuildConfig.Analyzer.csproj", "{517677D7-7299-426F-B1A3-47BDCC2F1214}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{4B1CEB70-F756-4A57-AAE8-8CD78C475F25}" - ProjectSection(SolutionItems) = preProject - ..\.github\CONTRIBUTING.md = ..\.github\CONTRIBUTING.md - ..\.github\SUPPORT.md = ..\.github\SUPPORT.md - EndProjectSection +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Mods.ConsoleCommands", "SMAPI.Mods.ConsoleCommands\SMAPI.Mods.ConsoleCommands.csproj", "{0634EA4C-3B8F-42DB-AEA6-CA9E4EF6E92F}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ISSUE_TEMPLATE", "ISSUE_TEMPLATE", "{F4453AB6-D7D6-447F-A973-956CC777968F}" - ProjectSection(SolutionItems) = preProject - ..\.github\ISSUE_TEMPLATE\bug_report.md = ..\.github\ISSUE_TEMPLATE\bug_report.md - ..\.github\ISSUE_TEMPLATE\feature_request.md = ..\.github\ISSUE_TEMPLATE\feature_request.md - ..\.github\ISSUE_TEMPLATE\general.md = ..\.github\ISSUE_TEMPLATE\general.md - EndProjectSection +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Mods.SaveBackup", "SMAPI.Mods.SaveBackup\SMAPI.Mods.SaveBackup.csproj", "{CD53AD6F-97F4-4872-A212-50C2A0FD3601}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Toolkit", "SMAPI.Toolkit\SMAPI.Toolkit.csproj", "{08184F74-60AD-4EEE-A78C-F4A35ADE6246}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Toolkit.CoreInterfaces", "SMAPI.Toolkit.CoreInterfaces\SMAPI.Toolkit.CoreInterfaces.csproj", "{ED8E41FA-DDFA-4A77-932E-9853D279A129}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Web", "SMAPI.Web\SMAPI.Web.csproj", "{80EFD92F-728F-41E0-8A5B-9F6F49A91899}" EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution @@ -80,61 +80,62 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {1298F2B2-57BD-4647-AF70-1FCBBEE500B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1298F2B2-57BD-4647-AF70-1FCBBEE500B6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1298F2B2-57BD-4647-AF70-1FCBBEE500B6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1298F2B2-57BD-4647-AF70-1FCBBEE500B6}.Release|Any CPU.Build.0 = Release|Any CPU - {0ED5EAD8-5D85-420D-8101-6D8CCCE29C9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0ED5EAD8-5D85-420D-8101-6D8CCCE29C9B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0ED5EAD8-5D85-420D-8101-6D8CCCE29C9B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0ED5EAD8-5D85-420D-8101-6D8CCCE29C9B}.Release|Any CPU.Build.0 = Release|Any CPU - {E023DA12-5960-4101-80B9-A7DCE955725C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E023DA12-5960-4101-80B9-A7DCE955725C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E023DA12-5960-4101-80B9-A7DCE955725C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E023DA12-5960-4101-80B9-A7DCE955725C}.Release|Any CPU.Build.0 = Release|Any CPU - {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|Any CPU.Build.0 = Release|Any CPU - {C11D0AFB-2893-41A9-AD55-D002F032D6AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C11D0AFB-2893-41A9-AD55-D002F032D6AD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C11D0AFB-2893-41A9-AD55-D002F032D6AD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C11D0AFB-2893-41A9-AD55-D002F032D6AD}.Release|Any CPU.Build.0 = Release|Any CPU - {80AD8528-AA49-4731-B4A6-C691845815A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {80AD8528-AA49-4731-B4A6-C691845815A1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {80AD8528-AA49-4731-B4A6-C691845815A1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {80AD8528-AA49-4731-B4A6-C691845815A1}.Release|Any CPU.Build.0 = Release|Any CPU - {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Release|Any CPU.Build.0 = Release|Any CPU - {EA5CFD2E-9453-4D29-B80F-8E0EA23F4AC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EA5CFD2E-9453-4D29-B80F-8E0EA23F4AC6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EA5CFD2E-9453-4D29-B80F-8E0EA23F4AC6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EA5CFD2E-9453-4D29-B80F-8E0EA23F4AC6}.Release|Any CPU.Build.0 = Release|Any CPU - {D5CFD923-37F1-4BC3-9BE8-E506E202AC28}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D5CFD923-37F1-4BC3-9BE8-E506E202AC28}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D5CFD923-37F1-4BC3-9BE8-E506E202AC28}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D5CFD923-37F1-4BC3-9BE8-E506E202AC28}.Release|Any CPU.Build.0 = Release|Any CPU - {8C2CA4AB-BA8A-446A-B59E-9D6502E145F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8C2CA4AB-BA8A-446A-B59E-9D6502E145F7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8C2CA4AB-BA8A-446A-B59E-9D6502E145F7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8C2CA4AB-BA8A-446A-B59E-9D6502E145F7}.Release|Any CPU.Build.0 = Release|Any CPU - {E4113F3E-3CAA-4288-9378-39A77BA625DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E4113F3E-3CAA-4288-9378-39A77BA625DB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E4113F3E-3CAA-4288-9378-39A77BA625DB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E4113F3E-3CAA-4288-9378-39A77BA625DB}.Release|Any CPU.Build.0 = Release|Any CPU + {680B2641-81EA-467C-86A5-0E81CDC57ED0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {680B2641-81EA-467C-86A5-0E81CDC57ED0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {680B2641-81EA-467C-86A5-0E81CDC57ED0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {680B2641-81EA-467C-86A5-0E81CDC57ED0}.Release|Any CPU.Build.0 = Release|Any CPU + {AA95884B-7097-476E-92C8-D0500DE9D6D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AA95884B-7097-476E-92C8-D0500DE9D6D1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AA95884B-7097-476E-92C8-D0500DE9D6D1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AA95884B-7097-476E-92C8-D0500DE9D6D1}.Release|Any CPU.Build.0 = Release|Any CPU + {E6DA2198-7686-4F1D-B312-4A4DC70884C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E6DA2198-7686-4F1D-B312-4A4DC70884C0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E6DA2198-7686-4F1D-B312-4A4DC70884C0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E6DA2198-7686-4F1D-B312-4A4DC70884C0}.Release|Any CPU.Build.0 = Release|Any CPU + {0A9BB24F-15FF-4C26-B1A2-81F7AE316518}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0A9BB24F-15FF-4C26-B1A2-81F7AE316518}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0A9BB24F-15FF-4C26-B1A2-81F7AE316518}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0A9BB24F-15FF-4C26-B1A2-81F7AE316518}.Release|Any CPU.Build.0 = Release|Any CPU + {1B3821E6-D030-402C-B3A1-7CA45C2800EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1B3821E6-D030-402C-B3A1-7CA45C2800EA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1B3821E6-D030-402C-B3A1-7CA45C2800EA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1B3821E6-D030-402C-B3A1-7CA45C2800EA}.Release|Any CPU.Build.0 = Release|Any CPU + {517677D7-7299-426F-B1A3-47BDCC2F1214}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {517677D7-7299-426F-B1A3-47BDCC2F1214}.Debug|Any CPU.Build.0 = Debug|Any CPU + {517677D7-7299-426F-B1A3-47BDCC2F1214}.Release|Any CPU.ActiveCfg = Release|Any CPU + {517677D7-7299-426F-B1A3-47BDCC2F1214}.Release|Any CPU.Build.0 = Release|Any CPU + {0634EA4C-3B8F-42DB-AEA6-CA9E4EF6E92F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0634EA4C-3B8F-42DB-AEA6-CA9E4EF6E92F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0634EA4C-3B8F-42DB-AEA6-CA9E4EF6E92F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0634EA4C-3B8F-42DB-AEA6-CA9E4EF6E92F}.Release|Any CPU.Build.0 = Release|Any CPU + {CD53AD6F-97F4-4872-A212-50C2A0FD3601}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CD53AD6F-97F4-4872-A212-50C2A0FD3601}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CD53AD6F-97F4-4872-A212-50C2A0FD3601}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CD53AD6F-97F4-4872-A212-50C2A0FD3601}.Release|Any CPU.Build.0 = Release|Any CPU + {08184F74-60AD-4EEE-A78C-F4A35ADE6246}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {08184F74-60AD-4EEE-A78C-F4A35ADE6246}.Debug|Any CPU.Build.0 = Debug|Any CPU + {08184F74-60AD-4EEE-A78C-F4A35ADE6246}.Release|Any CPU.ActiveCfg = Release|Any CPU + {08184F74-60AD-4EEE-A78C-F4A35ADE6246}.Release|Any CPU.Build.0 = Release|Any CPU + {ED8E41FA-DDFA-4A77-932E-9853D279A129}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ED8E41FA-DDFA-4A77-932E-9853D279A129}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ED8E41FA-DDFA-4A77-932E-9853D279A129}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ED8E41FA-DDFA-4A77-932E-9853D279A129}.Release|Any CPU.Build.0 = Release|Any CPU + {80EFD92F-728F-41E0-8A5B-9F6F49A91899}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {80EFD92F-728F-41E0-8A5B-9F6F49A91899}.Debug|Any CPU.Build.0 = Debug|Any CPU + {80EFD92F-728F-41E0-8A5B-9F6F49A91899}.Release|Any CPU.ActiveCfg = Release|Any CPU + {80EFD92F-728F-41E0-8A5B-9F6F49A91899}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {E023DA12-5960-4101-80B9-A7DCE955725C} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} - {EB35A917-67B9-4EFA-8DFC-4FB49B3949BB} = {86C452BE-D2D8-45B4-B63F-E329EB06CEDA} - {09CF91E5-5BAB-4650-A200-E5EA9A633046} = {86C452BE-D2D8-45B4-B63F-E329EB06CEDA} - {0CF97929-B0D0-4D73-B7BF-4FF7191035F9} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} {4B1CEB70-F756-4A57-AAE8-8CD78C475F25} = {86C452BE-D2D8-45B4-B63F-E329EB06CEDA} {F4453AB6-D7D6-447F-A973-956CC777968F} = {4B1CEB70-F756-4A57-AAE8-8CD78C475F25} + {09CF91E5-5BAB-4650-A200-E5EA9A633046} = {86C452BE-D2D8-45B4-B63F-E329EB06CEDA} + {EB35A917-67B9-4EFA-8DFC-4FB49B3949BB} = {86C452BE-D2D8-45B4-B63F-E329EB06CEDA} + {85208F8D-6FD1-4531-BE05-7142490F59FE} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} + {680B2641-81EA-467C-86A5-0E81CDC57ED0} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} + {AA95884B-7097-476E-92C8-D0500DE9D6D1} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {70143042-A862-47A8-A677-7C819DDC90DC} diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj new file mode 100644 index 00000000..58a71f94 --- /dev/null +++ b/src/SMAPI/SMAPI.csproj @@ -0,0 +1,60 @@ + + + + StardewModdingAPI + StardewModdingAPI + net45 + false + latest + x86 + Exe + $(SolutionDir)\..\bin\$(Configuration)\SMAPI + $(SolutionDir)\..\bin\$(Configuration)\SMAPI\StardewModdingAPI.xml + false + true + icon.ico + + + + + + + + + + + + True + + + True + + + + + + + + + + + + + + + + PreserveNewest + + + StardewModdingAPI.metadata.json + PreserveNewest + + + PreserveNewest + + + + + + + diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj deleted file mode 100644 index 718366a7..00000000 --- a/src/SMAPI/StardewModdingAPI.csproj +++ /dev/null @@ -1,60 +0,0 @@ - - - - StardewModdingAPI - StardewModdingAPI - net45 - false - latest - x86 - Exe - $(SolutionDir)\..\bin\$(Configuration)\SMAPI - $(SolutionDir)\..\bin\$(Configuration)\SMAPI\StardewModdingAPI.xml - false - true - icon.ico - - - - - - - - - - - - True - - - True - - - - - - - - - - - - - - - - PreserveNewest - - - StardewModdingAPI.metadata.json - PreserveNewest - - - PreserveNewest - - - - - - - -- cgit From c15785a68d3e99959cdcca5bfb51e8686316a33b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 24 Apr 2019 23:46:52 -0400 Subject: simplify config.json and metadata.json names --- build/common.targets | 4 +- build/prepare-install-package.targets | 6 +- docs/technical-docs.md | 4 +- src/SMAPI.Installer/Framework/InstallerPaths.cs | 2 +- src/SMAPI.Installer/InteractiveInstaller.cs | 3 +- src/SMAPI.Installer/README.txt | 4 +- src/SMAPI.Web/Controllers/ModsApiController.cs | 2 +- src/SMAPI.Web/SMAPI.Web.csproj | 2 +- src/SMAPI.Web/Views/Index/Privacy.cshtml | 2 +- src/SMAPI.Web/wwwroot/SMAPI.metadata.json | 507 +++++++++++++++++++++ .../wwwroot/StardewModdingAPI.metadata.json | 507 --------------------- src/SMAPI/Constants.cs | 4 +- src/SMAPI/SMAPI.config.json | 77 ++++ src/SMAPI/SMAPI.csproj | 6 +- src/SMAPI/StardewModdingAPI.config.json | 77 ---- 15 files changed, 603 insertions(+), 604 deletions(-) create mode 100644 src/SMAPI.Web/wwwroot/SMAPI.metadata.json delete mode 100644 src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json create mode 100644 src/SMAPI/SMAPI.config.json delete mode 100644 src/SMAPI/StardewModdingAPI.config.json (limited to 'src') diff --git a/build/common.targets b/build/common.targets index 9922bf15..b42ef063 100644 --- a/build/common.targets +++ b/build/common.targets @@ -118,8 +118,8 @@ - - + + diff --git a/build/prepare-install-package.targets b/build/prepare-install-package.targets index 928191a2..18a69a24 100644 --- a/build/prepare-install-package.targets +++ b/build/prepare-install-package.targets @@ -38,8 +38,8 @@ - - + + @@ -76,7 +76,7 @@ - + diff --git a/docs/technical-docs.md b/docs/technical-docs.md index 545f4aa3..5598b7a8 100644 --- a/docs/technical-docs.md +++ b/docs/technical-docs.md @@ -70,8 +70,8 @@ on the wiki for the first-time setup. ## Customisation ### Configuration file -You can customise the SMAPI behaviour by editing the `smapi-internal/StardewModdingAPI.config.json` -file in your game folder. +You can customise the SMAPI behaviour by editing the `smapi-internal/config.json` file in your game +folder. Basic fields: diff --git a/src/SMAPI.Installer/Framework/InstallerPaths.cs b/src/SMAPI.Installer/Framework/InstallerPaths.cs index e5396018..9393e14f 100644 --- a/src/SMAPI.Installer/Framework/InstallerPaths.cs +++ b/src/SMAPI.Installer/Framework/InstallerPaths.cs @@ -59,7 +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"); + this.ApiConfigPath = Path.Combine(gameDir.FullName, "smapi-internal", "config.json"); } } } diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index 7148b1d9..c8d36b01 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -112,6 +112,7 @@ namespace StardewModdingApi.Installer yield return GetInstallPath("StardewModdingAPI.pdb"); // Windows only yield return GetInstallPath("StardewModdingAPI.xml"); yield return GetInstallPath("smapi-internal"); + yield return GetInstallPath("steam_appid.txt"); // obsolete yield return GetInstallPath(Path.Combine("Mods", ".cache")); // 1.3-1.4 @@ -133,11 +134,9 @@ namespace StardewModdingApi.Installer 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) { diff --git a/src/SMAPI.Installer/README.txt b/src/SMAPI.Installer/README.txt index 79c90cc0..0da49a46 100644 --- a/src/SMAPI.Installer/README.txt +++ b/src/SMAPI.Installer/README.txt @@ -40,5 +40,5 @@ 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. +- To configure the color scheme, edit the `smapi-internal/config.json` file and see instructions + there for the 'ColorScheme' setting. diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 7e6f592c..9cffd3b3 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -65,7 +65,7 @@ namespace StardewModdingAPI.Web.Controllers /// The Nexus API client. public ModsApiController(IHostingEnvironment environment, IMemoryCache cache, IOptions configProvider, IChucklefishClient chucklefish, IGitHubClient github, IModDropClient modDrop, INexusClient nexus) { - this.ModDatabase = new ModToolkit().GetModDatabase(Path.Combine(environment.WebRootPath, "StardewModdingAPI.metadata.json")); + this.ModDatabase = new ModToolkit().GetModDatabase(Path.Combine(environment.WebRootPath, "SMAPI.metadata.json")); ModUpdateCheckConfig config = configProvider.Value; this.CompatibilityPageUrl = config.CompatibilityPageUrl; diff --git a/src/SMAPI.Web/SMAPI.Web.csproj b/src/SMAPI.Web/SMAPI.Web.csproj index 023a9333..1d8e41c6 100644 --- a/src/SMAPI.Web/SMAPI.Web.csproj +++ b/src/SMAPI.Web/SMAPI.Web.csproj @@ -38,7 +38,7 @@ $(IncludeRazorContentInPack) - + PreserveNewest diff --git a/src/SMAPI.Web/Views/Index/Privacy.cshtml b/src/SMAPI.Web/Views/Index/Privacy.cshtml index ca99eef6..356580b4 100644 --- a/src/SMAPI.Web/Views/Index/Privacy.cshtml +++ b/src/SMAPI.Web/Views/Index/Privacy.cshtml @@ -29,7 +29,7 @@

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

  1. find your game folder;
  2. -
  3. open the smapi-internal/StardewModdingAPI.config.json file in a text editor;
  4. +
  5. open the smapi-internal/config.json file in a text editor;
  6. change "CheckForUpdates": true to "CheckForUpdates": false.
diff --git a/src/SMAPI.Web/wwwroot/SMAPI.metadata.json b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json new file mode 100644 index 00000000..f1e0023e --- /dev/null +++ b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json @@ -0,0 +1,507 @@ +{ + /** + * Metadata about some SMAPI mods used in compatibility, update, and dependency checks. This + * field shouldn't be edited by players in most cases. + * + * Standard fields + * =============== + * The predefined fields are documented below (only 'ID' is required). Each entry's key is the + * default display name for the mod if one isn't available (e.g. in dependency checks). + * + * - ID: the mod's latest unique ID (if any). + * + * - FormerIDs: uniquely identifies the mod across multiple versions, and supports matching + * other fields if no ID was specified. This doesn't include the latest ID, if any. Multiple + * variants can be separated with '|'. + * + * - MapLocalVersions and MapRemoteVersions correct local manifest versions and remote versions + * during update checks. For example, if the API returns version '1.1-1078' where '1078' is + * intended to be a build number, MapRemoteVersions can map it to '1.1' when comparing to the + * mod's current version. This is only meant to support legacy mods with injected update keys. + * + * Versioned metadata + * ================== + * Each record can also specify extra metadata using the field keys below. + * + * Each key consists of a field name prefixed with any combination of version range and 'Default', + * separated by pipes (whitespace trimmed). For example, 'UpdateKey' will always override, + * 'Default | UpdateKey' will only override if the mod has no update keys, and + * '~1.1 | Default | Name' will do the same up to version 1.1. + * + * The version format is 'min~max' (where either side can be blank for unbounded), or a single + * version number. + * + * These are the valid field names: + * + * - UpdateKey: the update key to set in the mod's manifest. This is used to enable update + * checks for older mods that haven't been updated to use it yet. + * + * - Status: overrides compatibility checks. The possible values are Obsolete (SMAPI won't load + * it because the mod should no longer be used), AssumeBroken (SMAPI won't load it because + * the specified version isn't compatible), or AssumeCompatible (SMAPI will try to load it + * even if it detects incompatible code). + * + * Note that this shouldn't be set to 'AssumeBroken' if SMAPI can detect the incompatibility + * automatically, since that hides the details from trace logs. + * + * - StatusReasonPhrase: a message to show to the player explaining why the mod can't be loaded + * (if applicable). If blank, will default to a generic not-compatible message. + * + * - AlternativeUrl: a URL where the player can find an unofficial update or alternative if the + * mod is no longer compatible. + */ + "ModData": { + /********* + ** Common dependencies for friendly errors + *********/ + "Advanced Location Loader": { + "ID": "Entoarox.AdvancedLocationLoader", + "Default | UpdateKey": "Nexus:2270" + }, + + //"Content Patcher": { + // "ID": "Pathoschild.ContentPatcher", + // "Default | UpdateKey": "Nexus:1915" + //}, + + //"Custom Farming Redux": { + // "ID": "Platonymous.CustomFarming", + // "Default | UpdateKey": "Nexus:991" + //}, + + "Custom Shirts": { + "ID": "Platonymous.CustomShirts", + "Default | UpdateKey": "Nexus:2416" + }, + + "Entoarox Framework": { + "ID": "Entoarox.EntoaroxFramework", + "Default | UpdateKey": "Nexus:2269" + }, + + "JSON Assets": { + "ID": "spacechase0.JsonAssets", + "Default | UpdateKey": "Nexus:1720", + "1.3.1 | Status": "AssumeBroken" // causes runtime crashes + }, + + "Mail Framework": { + "ID": "DIGUS.MailFrameworkMod", + "Default | UpdateKey": "Nexus:1536" + }, + + "MTN": { + "ID": "SgtPickles.MTN", + "Default | UpdateKey": "Nexus:2256", + "~1.2.6 | Status": "AssumeBroken" // replaces Game1.multiplayer, which breaks SMAPI's multiplayer API. + }, + + "PyTK": { + "ID": "Platonymous.Toolkit", + "Default | UpdateKey": "Nexus:1726" + }, + + "Rubydew": { + "ID": "bwdy.rubydew", + "SuppressWarnings": "UsesDynamic", // mod explicitly loads DLLs for Linux/Mac compatibility + "Default | UpdateKey": "Nexus:3656" + }, + + "SpaceCore": { + "ID": "spacechase0.SpaceCore", + "Default | UpdateKey": "Nexus:1348" + }, + + "Stardust Core": { + "ID": "Omegasis.StardustCore", + "Default | UpdateKey": "Nexus:2341" + }, + + "TMX Loader": { + "ID": "Platonymous.TMXLoader", + "Default | UpdateKey": "Nexus:1820" + }, + + + /********* + ** Map versions + *********/ + "Adjust Artisan Prices": { + "ID": "ThatNorthernMonkey.AdjustArtisanPrices", + "FormerIDs": "1e36d4ca-c7ef-4dfb-9927-d27a6c3c8bdc", // changed in 0.0.2-pathoschild-update + "MapRemoteVersions": { "0.01": "0.0.1" } + }, + + "Almighty Farming Tool": { + "ID": "439", + "MapRemoteVersions": { + "1.21": "1.2.1", + "1.22-unofficial.3.mizzion": "1.2.2-unofficial.3.mizzion" + } + }, + + "Basic Sprinkler Improved": { + "ID": "lrsk_sdvm_bsi.0117171308", + "MapRemoteVersions": { "1.0.2": "1.0.1-release" } // manifest not updated + }, + + "Better Shipping Box": { + "ID": "Kithio:BetterShippingBox", + "MapLocalVersions": { "1.0.1": "1.0.2" } + }, + + "Chefs Closet": { + "ID": "Duder.ChefsCloset", + "MapLocalVersions": { "1.3-1": "1.3" } + }, + + "Configurable Machines": { + "ID": "21da6619-dc03-4660-9794-8e5b498f5b97", + "MapLocalVersions": { "1.2-beta": "1.2" } + }, + + "Crafting Counter": { + "ID": "lolpcgaming.CraftingCounter", + "MapRemoteVersions": { "1.1": "1.0" } // not updated in manifest + }, + + "Custom Linens": { + "ID": "Mevima.CustomLinens", + "MapRemoteVersions": { "1.1": "1.0" } // manifest not updated + }, + + "Dynamic Horses": { + "ID": "Bpendragon-DynamicHorses", + "MapRemoteVersions": { "1.2": "1.1-release" } // manifest not updated + }, + + "Dynamic Machines": { + "ID": "DynamicMachines", + "MapLocalVersions": { "1.1": "1.1.1" } + }, + + "Multiple Sprites and Portraits On Rotation (File Loading)": { + "ID": "FileLoading", + "MapLocalVersions": { "1.1": "1.12" } + }, + + "Relationship Status": { + "ID": "relationshipstatus", + "MapRemoteVersions": { "1.0.5": "1.0.4" } // not updated in manifest + }, + + "ReRegeneration": { + "ID": "lrsk_sdvm_rerg.0925160827", + "MapLocalVersions": { "1.1.2-release": "1.1.2" } + }, + + "Showcase Mod": { + "ID": "Igorious.Showcase", + "MapLocalVersions": { "0.9-500": "0.9" } + }, + + "Siv's Marriage Mod": { + "ID": "6266959802", // official version + "FormerIDs": "Siv.MarriageMod | medoli900.Siv's Marriage Mod", // 1.2.3-unofficial versions + "MapLocalVersions": { "0.0": "1.4" } + }, + + + /********* + ** Obsolete + *********/ + "Animal Mood Fix": { + "ID": "GPeters-AnimalMoodFix", + "~ | Status": "Obsolete", + "~ | StatusReasonPhrase": "the animal mood bugs were fixed in Stardew Valley 1.2." + }, + + "Colored Chests": { + "ID": "4befde5c-731c-4853-8e4b-c5cdf946805f", + "~ | Status": "Obsolete", + "~ | StatusReasonPhrase": "colored chests were added in Stardew Valley 1.1." + }, + + "Modder Serialization Utility": { + "ID": "SerializerUtils-0-1", + "~ | Status": "Obsolete", + "~ | StatusReasonPhrase": "it's no longer maintained or used." + }, + + "No Debug Mode": { + "ID": "NoDebugMode", + "~ | Status": "Obsolete", + "~ | StatusReasonPhrase": "debug mode was removed in SMAPI 1.0." + }, + + /********* + ** Broke in SDV 1.4 + *********/ + "Loved Labels": { + "ID": "Advize.LovedLabels", + "~2.2.1-unofficial.2-pathoschild | Status": "AssumeBroken" // runtime reflection errors + }, + + /********* + ** Broke in SMAPI 3.0 (runtime errors due to lifecycle changes) + *********/ + "Advancing Sprinklers": { + "ID": "warix3.advancingsprinklers", + "~1.0.0 | Status": "AssumeBroken" + }, + + "Arcade 2048": { + "ID": "Platonymous.2048", + "~1.0.6 | Status": "AssumeBroken" // possibly due to PyTK + }, + + "Arcade Snake": { + "ID": "Platonymous.Snake", + "~1.1.0 | Status": "AssumeBroken" // possibly due to PyTK + }, + + "Better Sprinklers": { + "ID": "Speeder.BetterSprinklers", + "~2.3.1-unofficial.7-pathoschild | Status": "AssumeBroken" + }, + + "Content Patcher": { + "ID": "Pathoschild.ContentPatcher", + "Default | UpdateKey": "Nexus:1915", + "~1.6.4 | Status": "AssumeBroken" + }, + + "Custom Adventure Guild Challenges": { + "ID": "DefenTheNation.CustomGuildChallenges", + "~1.8 | Status": "AssumeBroken" + }, + + "Custom Farming Redux": { + "ID": "Platonymous.CustomFarming", + "Default | UpdateKey": "Nexus:991", + "~2.10.10 | Status": "AssumeBroken" // possibly due to PyTK + }, + + "JoJaBan - Arcade Sokoban": { + "ID": "Platonymous.JoJaBan", + "~0.4.3 | Status": "AssumeBroken" // possibly due to PyTK + }, + + "Level Extender": { + "ID": "DevinLematty.LevelExtender", + "~3.1 | Status": "AssumeBroken" + }, + + "Mod Update Menu": { + "ID": "cat.modupdatemenu", + "~1.4 | Status": "AssumeBroken" + }, + + "Quick Start": { + "ID": "WuestMan.QuickStart", + "~1.5 | Status": "AssumeBroken" + }, + + "Seed Bag": { + "ID": "Platonymous.SeedBag", + "~1.2.7 | Status": "AssumeBroken" // possibly due to PyTK + }, + + /********* + ** Broke in SDV 1.3.36 + *********/ + "2cute FarmCave": { + "ID": "taintedwheat.2CuteFarmCave", + "Default | UpdateKey": "Nexus:843", + "~2.0 | Status": "AssumeBroken" // references deleted Content/Mine.xnb + }, + + "Ace's Expanded Caves - Default Cave": { + "ID": "Acerbicon.AECdefault", + "Default | UpdateKey": "Nexus:2131", + "~1.2.2 | Status": "AssumeBroken" // references deleted Content/Mine.xnb + }, + + "Ace's Expanded Caves - Desert Cave": { + "ID": "Acerbicon.AECdesert", + "Default | UpdateKey": "Nexus:2131", + "~1.2.2 | Status": "AssumeBroken" // references deleted Content/Mine.xnb + }, + + "Ace's Expanded Caves - Ice Cave": { + "ID": "Acerbicon.AECice", + "Default | UpdateKey": "Nexus:2131", + "~1.2.2 | Status": "AssumeBroken" // references deleted Content/Mine.xnb + }, + + "Ace's Expanded Caves - Lava Cave": { + "ID": "Acerbicon.AEClava", + "Default | UpdateKey": "Nexus:2131", + "~1.2.2 | Status": "AssumeBroken" // references deleted Content/Mine.xnb + }, + + "Ace's Expanded Caves - Slime Cave": { + "ID": "Acerbicon.AECslime", + "Default | UpdateKey": "Nexus:2131", + "~1.2.2 | Status": "AssumeBroken" // references deleted Content/Mine.xnb + }, + + "Green Pastures Farm": { + "ID": "bugbuddy.GreenPasturesFarm", + "Default | UpdateKey": "Nexus:2326", + "~1.0 | Status": "AssumeBroken" // references deleted Content/weapons.xnb + }, + + "Immersive Farm 2": { + "ID": "zander.immersivefarm2", + "~2.0.1 | Status": "AssumeBroken" // references deleted Content/Mine.xnb + }, + + "Karmylla's Immersive Map Edits": { + "ID": "Karmylla.ImmersiveMapEdits", + "Default | UpdateKey": "Nexus:1149", + "~2.4 | Status": "AssumeBroken" // references deleted Content/weapons.xnb + }, + + "Secret Gardens Greenhouse": { + "ID": "jessebot.secretgardens", + "Default | UpdateKey": "Nexus:3067", + "~2.0.1 | Status": "AssumeBroken" // references deleted Content/Mine.xnb + }, + + /********* + ** Broke circa SDV 1.3 + *********/ + "Canon-Friendly Dialogue Expansion": { + "ID": "gizzymo.canonfriendlyexpansion", + "~1.1.1 | Status": "AssumeBroken" // causes a save crash on certain dates + }, + + "Everytime Submarine": { + "ID": "MustafaDemirel.EverytimeSubmarine", + "~1.0.0 | Status": "AssumeBroken" // breaks player saves if their beach bridge is fixed + }, + + "Always Scroll Map": { + "ID": "bcmpinc.AlwaysScrollMap", + "~0.6 | Status": "AssumeBroken" // breaks newer versions of bcmpinc mods (per bcmpinc's request) + }, + + "Arcade Pong": { + "ID": "Platonymous.ArcadePong", + "~1.0.2 | Status": "AssumeBroken" // broke in SMAPI 2.6-beta.16 due to reflection into SMAPI internals + }, + + "BJS Night Sounds": { + "ID": "BunnyJumps.BJSNightSounds", + "~1.0.0 | Status": "AssumeBroken" // runtime errors with Harmony 1.2.0.1 in SMAPI 2.8+ + }, + + "Craft Counter": { + "ID": "bcmpinc.CraftCounter", + "~0.6 | Status": "AssumeBroken" // breaks newer versions of bcmpinc mods (per bcmpinc's request) + }, + + "Fishing Adjust": { + "ID": "shuaiz.FishingAdjustMod", + "~2.0.1 | Status": "AssumeBroken" // Method not found: 'Void Harmony.HarmonyInstance.Patch(System.Reflection.MethodBase, Harmony.HarmonyMethod, Harmony.HarmonyMethod, Harmony.HarmonyMethod)' + }, + + "Fishing Automaton": { + "ID": "Drynwynn.FishingAutomaton", + "~1.1 | Status": "AssumeBroken" // runtime errors with Harmony 1.2.0.1 in SMAPI 2.8+ + }, + + "Fix Animal Tools": { + "ID": "bcmpinc.FixAnimalTools", + "~0.6 | Status": "AssumeBroken" // breaks newer versions of bcmpinc mods (per bcmpinc's request) + }, + + "Fix Scythe Exp": { + "ID": "bcmpinc.FixScytheExp", + "~0.3 | Status": "AssumeBroken" // broke in 1.3: Exception from HarmonyInstance "bcmpinc.FixScytheExp" [...] Bad label content in ILGenerator. + }, + + "Grass Growth": { + "ID": "bcmpinc.GrassGrowth", + "~0.6 | Status": "AssumeBroken" // breaks newer versions of bcmpinc mods (per bcmpinc's request) + }, + + "More Silo Storage": { + "ID": "OrneryWalrus.MoreSiloStorage", + "~1.0.1 | Status": "AssumeBroken" // broke in SDV 1.3 + }, + + "Movement Speed": { + "ID": "bcmpinc.MovementSpeed", + "~0.6 | Status": "AssumeBroken" // breaks newer versions of bcmpinc mods (per bcmpinc's request) + }, + + "No Added Flying Mine Monsters": { + "ID": "Drynwynn.NoAddedFlyingMineMonsters", + "~1.1 | Status": "AssumeBroken" // runtime errors with Harmony 1.2.0.1 in SMAPI 2.8+ + }, + + "Server Bookmarker": { + "ID": "Ilyaki.ServerBookmarker", + "~1.0.0 | Status": "AssumeBroken" // broke in Stardew Valley 1.3.29 (runtime errors) + }, + + "Skill Prestige: Cooking Adapter": { + "ID": "Alphablackwolf.CookingSkillPrestigeAdapter", + "FormerIDs": "20d6b8a3-b6e7-460b-a6e4-07c2b0cb6c63", // changed circa 1.1 + "MapRemoteVersions": { "1.2.3": "1.1" } // manifest not updated + }, + + "Skull Cave Saver": { + "ID": "cantorsdust.SkullCaveSaver", + "FormerIDs": "8ac06349-26f7-4394-806c-95d48fd35774 | community.SkullCaveSaver", // changed in 1.1 and 1.2.2 + "1.3-beta | Status": "AssumeBroken" // doesn't work in multiplayer, no longer maintained + }, + + "Split Screen": { + "ID": "Ilyaki.SplitScreen", + "~3.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.6-beta.16 due to reflection into SMAPI internals + }, + + "Stardew Hack": { + "ID": "bcmpinc.StardewHack", + "~0.6 | Status": "AssumeBroken" // breaks newer versions of bcmpinc mods (per bcmpinc's request) + }, + + "Stephan's Lots of Crops": { + "ID": "stephansstardewcrops", + "MapRemoteVersions": { "1.41": "1.1" }, // manifest not updated + "~1.1 | Status": "AssumeBroken" // broke in SDV 1.3 (overwrites vanilla items) + }, + + "Summit Reborn": { + "ID": "KoihimeNakamura.summitreborn", + "FormerIDs": "emissaryofinfinity.summitreborn", // changed in 1.0.2 + "~1.0.2 | Status": "AssumeBroken" // broke in SDV 1.3 (runtime errors) + }, + + "Tilled Soil Decay": { + "ID": "bcmpinc.TilledSoilDecay", + "~0.6 | Status": "AssumeBroken" // breaks newer versions of bcmpinc mods (per bcmpinc's request) + }, + + "Tree Spread": { + "ID": "bcmpinc.TreeSpread", + "~0.6 | Status": "AssumeBroken" // breaks newer versions of bcmpinc mods (per bcmpinc's request) + }, + + "Yet Another Harvest With Scythe Mod": { + "ID": "bcmpinc.HarvestWithScythe", + "~0.6 | Status": "AssumeBroken" // breaks newer versions of bcmpinc mods (per bcmpinc's request) + }, + + /********* + ** Broke circa SDV 1.2 + *********/ + "Move Faster": { + "ID": "shuaiz.MoveFasterMod", + "~1.0.1 | Status": "AssumeBroken" // doesn't do anything as of SDV 1.2.33 (bad Harmony patch?) + } + } +} diff --git a/src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json b/src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json deleted file mode 100644 index f1e0023e..00000000 --- a/src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json +++ /dev/null @@ -1,507 +0,0 @@ -{ - /** - * Metadata about some SMAPI mods used in compatibility, update, and dependency checks. This - * field shouldn't be edited by players in most cases. - * - * Standard fields - * =============== - * The predefined fields are documented below (only 'ID' is required). Each entry's key is the - * default display name for the mod if one isn't available (e.g. in dependency checks). - * - * - ID: the mod's latest unique ID (if any). - * - * - FormerIDs: uniquely identifies the mod across multiple versions, and supports matching - * other fields if no ID was specified. This doesn't include the latest ID, if any. Multiple - * variants can be separated with '|'. - * - * - MapLocalVersions and MapRemoteVersions correct local manifest versions and remote versions - * during update checks. For example, if the API returns version '1.1-1078' where '1078' is - * intended to be a build number, MapRemoteVersions can map it to '1.1' when comparing to the - * mod's current version. This is only meant to support legacy mods with injected update keys. - * - * Versioned metadata - * ================== - * Each record can also specify extra metadata using the field keys below. - * - * Each key consists of a field name prefixed with any combination of version range and 'Default', - * separated by pipes (whitespace trimmed). For example, 'UpdateKey' will always override, - * 'Default | UpdateKey' will only override if the mod has no update keys, and - * '~1.1 | Default | Name' will do the same up to version 1.1. - * - * The version format is 'min~max' (where either side can be blank for unbounded), or a single - * version number. - * - * These are the valid field names: - * - * - UpdateKey: the update key to set in the mod's manifest. This is used to enable update - * checks for older mods that haven't been updated to use it yet. - * - * - Status: overrides compatibility checks. The possible values are Obsolete (SMAPI won't load - * it because the mod should no longer be used), AssumeBroken (SMAPI won't load it because - * the specified version isn't compatible), or AssumeCompatible (SMAPI will try to load it - * even if it detects incompatible code). - * - * Note that this shouldn't be set to 'AssumeBroken' if SMAPI can detect the incompatibility - * automatically, since that hides the details from trace logs. - * - * - StatusReasonPhrase: a message to show to the player explaining why the mod can't be loaded - * (if applicable). If blank, will default to a generic not-compatible message. - * - * - AlternativeUrl: a URL where the player can find an unofficial update or alternative if the - * mod is no longer compatible. - */ - "ModData": { - /********* - ** Common dependencies for friendly errors - *********/ - "Advanced Location Loader": { - "ID": "Entoarox.AdvancedLocationLoader", - "Default | UpdateKey": "Nexus:2270" - }, - - //"Content Patcher": { - // "ID": "Pathoschild.ContentPatcher", - // "Default | UpdateKey": "Nexus:1915" - //}, - - //"Custom Farming Redux": { - // "ID": "Platonymous.CustomFarming", - // "Default | UpdateKey": "Nexus:991" - //}, - - "Custom Shirts": { - "ID": "Platonymous.CustomShirts", - "Default | UpdateKey": "Nexus:2416" - }, - - "Entoarox Framework": { - "ID": "Entoarox.EntoaroxFramework", - "Default | UpdateKey": "Nexus:2269" - }, - - "JSON Assets": { - "ID": "spacechase0.JsonAssets", - "Default | UpdateKey": "Nexus:1720", - "1.3.1 | Status": "AssumeBroken" // causes runtime crashes - }, - - "Mail Framework": { - "ID": "DIGUS.MailFrameworkMod", - "Default | UpdateKey": "Nexus:1536" - }, - - "MTN": { - "ID": "SgtPickles.MTN", - "Default | UpdateKey": "Nexus:2256", - "~1.2.6 | Status": "AssumeBroken" // replaces Game1.multiplayer, which breaks SMAPI's multiplayer API. - }, - - "PyTK": { - "ID": "Platonymous.Toolkit", - "Default | UpdateKey": "Nexus:1726" - }, - - "Rubydew": { - "ID": "bwdy.rubydew", - "SuppressWarnings": "UsesDynamic", // mod explicitly loads DLLs for Linux/Mac compatibility - "Default | UpdateKey": "Nexus:3656" - }, - - "SpaceCore": { - "ID": "spacechase0.SpaceCore", - "Default | UpdateKey": "Nexus:1348" - }, - - "Stardust Core": { - "ID": "Omegasis.StardustCore", - "Default | UpdateKey": "Nexus:2341" - }, - - "TMX Loader": { - "ID": "Platonymous.TMXLoader", - "Default | UpdateKey": "Nexus:1820" - }, - - - /********* - ** Map versions - *********/ - "Adjust Artisan Prices": { - "ID": "ThatNorthernMonkey.AdjustArtisanPrices", - "FormerIDs": "1e36d4ca-c7ef-4dfb-9927-d27a6c3c8bdc", // changed in 0.0.2-pathoschild-update - "MapRemoteVersions": { "0.01": "0.0.1" } - }, - - "Almighty Farming Tool": { - "ID": "439", - "MapRemoteVersions": { - "1.21": "1.2.1", - "1.22-unofficial.3.mizzion": "1.2.2-unofficial.3.mizzion" - } - }, - - "Basic Sprinkler Improved": { - "ID": "lrsk_sdvm_bsi.0117171308", - "MapRemoteVersions": { "1.0.2": "1.0.1-release" } // manifest not updated - }, - - "Better Shipping Box": { - "ID": "Kithio:BetterShippingBox", - "MapLocalVersions": { "1.0.1": "1.0.2" } - }, - - "Chefs Closet": { - "ID": "Duder.ChefsCloset", - "MapLocalVersions": { "1.3-1": "1.3" } - }, - - "Configurable Machines": { - "ID": "21da6619-dc03-4660-9794-8e5b498f5b97", - "MapLocalVersions": { "1.2-beta": "1.2" } - }, - - "Crafting Counter": { - "ID": "lolpcgaming.CraftingCounter", - "MapRemoteVersions": { "1.1": "1.0" } // not updated in manifest - }, - - "Custom Linens": { - "ID": "Mevima.CustomLinens", - "MapRemoteVersions": { "1.1": "1.0" } // manifest not updated - }, - - "Dynamic Horses": { - "ID": "Bpendragon-DynamicHorses", - "MapRemoteVersions": { "1.2": "1.1-release" } // manifest not updated - }, - - "Dynamic Machines": { - "ID": "DynamicMachines", - "MapLocalVersions": { "1.1": "1.1.1" } - }, - - "Multiple Sprites and Portraits On Rotation (File Loading)": { - "ID": "FileLoading", - "MapLocalVersions": { "1.1": "1.12" } - }, - - "Relationship Status": { - "ID": "relationshipstatus", - "MapRemoteVersions": { "1.0.5": "1.0.4" } // not updated in manifest - }, - - "ReRegeneration": { - "ID": "lrsk_sdvm_rerg.0925160827", - "MapLocalVersions": { "1.1.2-release": "1.1.2" } - }, - - "Showcase Mod": { - "ID": "Igorious.Showcase", - "MapLocalVersions": { "0.9-500": "0.9" } - }, - - "Siv's Marriage Mod": { - "ID": "6266959802", // official version - "FormerIDs": "Siv.MarriageMod | medoli900.Siv's Marriage Mod", // 1.2.3-unofficial versions - "MapLocalVersions": { "0.0": "1.4" } - }, - - - /********* - ** Obsolete - *********/ - "Animal Mood Fix": { - "ID": "GPeters-AnimalMoodFix", - "~ | Status": "Obsolete", - "~ | StatusReasonPhrase": "the animal mood bugs were fixed in Stardew Valley 1.2." - }, - - "Colored Chests": { - "ID": "4befde5c-731c-4853-8e4b-c5cdf946805f", - "~ | Status": "Obsolete", - "~ | StatusReasonPhrase": "colored chests were added in Stardew Valley 1.1." - }, - - "Modder Serialization Utility": { - "ID": "SerializerUtils-0-1", - "~ | Status": "Obsolete", - "~ | StatusReasonPhrase": "it's no longer maintained or used." - }, - - "No Debug Mode": { - "ID": "NoDebugMode", - "~ | Status": "Obsolete", - "~ | StatusReasonPhrase": "debug mode was removed in SMAPI 1.0." - }, - - /********* - ** Broke in SDV 1.4 - *********/ - "Loved Labels": { - "ID": "Advize.LovedLabels", - "~2.2.1-unofficial.2-pathoschild | Status": "AssumeBroken" // runtime reflection errors - }, - - /********* - ** Broke in SMAPI 3.0 (runtime errors due to lifecycle changes) - *********/ - "Advancing Sprinklers": { - "ID": "warix3.advancingsprinklers", - "~1.0.0 | Status": "AssumeBroken" - }, - - "Arcade 2048": { - "ID": "Platonymous.2048", - "~1.0.6 | Status": "AssumeBroken" // possibly due to PyTK - }, - - "Arcade Snake": { - "ID": "Platonymous.Snake", - "~1.1.0 | Status": "AssumeBroken" // possibly due to PyTK - }, - - "Better Sprinklers": { - "ID": "Speeder.BetterSprinklers", - "~2.3.1-unofficial.7-pathoschild | Status": "AssumeBroken" - }, - - "Content Patcher": { - "ID": "Pathoschild.ContentPatcher", - "Default | UpdateKey": "Nexus:1915", - "~1.6.4 | Status": "AssumeBroken" - }, - - "Custom Adventure Guild Challenges": { - "ID": "DefenTheNation.CustomGuildChallenges", - "~1.8 | Status": "AssumeBroken" - }, - - "Custom Farming Redux": { - "ID": "Platonymous.CustomFarming", - "Default | UpdateKey": "Nexus:991", - "~2.10.10 | Status": "AssumeBroken" // possibly due to PyTK - }, - - "JoJaBan - Arcade Sokoban": { - "ID": "Platonymous.JoJaBan", - "~0.4.3 | Status": "AssumeBroken" // possibly due to PyTK - }, - - "Level Extender": { - "ID": "DevinLematty.LevelExtender", - "~3.1 | Status": "AssumeBroken" - }, - - "Mod Update Menu": { - "ID": "cat.modupdatemenu", - "~1.4 | Status": "AssumeBroken" - }, - - "Quick Start": { - "ID": "WuestMan.QuickStart", - "~1.5 | Status": "AssumeBroken" - }, - - "Seed Bag": { - "ID": "Platonymous.SeedBag", - "~1.2.7 | Status": "AssumeBroken" // possibly due to PyTK - }, - - /********* - ** Broke in SDV 1.3.36 - *********/ - "2cute FarmCave": { - "ID": "taintedwheat.2CuteFarmCave", - "Default | UpdateKey": "Nexus:843", - "~2.0 | Status": "AssumeBroken" // references deleted Content/Mine.xnb - }, - - "Ace's Expanded Caves - Default Cave": { - "ID": "Acerbicon.AECdefault", - "Default | UpdateKey": "Nexus:2131", - "~1.2.2 | Status": "AssumeBroken" // references deleted Content/Mine.xnb - }, - - "Ace's Expanded Caves - Desert Cave": { - "ID": "Acerbicon.AECdesert", - "Default | UpdateKey": "Nexus:2131", - "~1.2.2 | Status": "AssumeBroken" // references deleted Content/Mine.xnb - }, - - "Ace's Expanded Caves - Ice Cave": { - "ID": "Acerbicon.AECice", - "Default | UpdateKey": "Nexus:2131", - "~1.2.2 | Status": "AssumeBroken" // references deleted Content/Mine.xnb - }, - - "Ace's Expanded Caves - Lava Cave": { - "ID": "Acerbicon.AEClava", - "Default | UpdateKey": "Nexus:2131", - "~1.2.2 | Status": "AssumeBroken" // references deleted Content/Mine.xnb - }, - - "Ace's Expanded Caves - Slime Cave": { - "ID": "Acerbicon.AECslime", - "Default | UpdateKey": "Nexus:2131", - "~1.2.2 | Status": "AssumeBroken" // references deleted Content/Mine.xnb - }, - - "Green Pastures Farm": { - "ID": "bugbuddy.GreenPasturesFarm", - "Default | UpdateKey": "Nexus:2326", - "~1.0 | Status": "AssumeBroken" // references deleted Content/weapons.xnb - }, - - "Immersive Farm 2": { - "ID": "zander.immersivefarm2", - "~2.0.1 | Status": "AssumeBroken" // references deleted Content/Mine.xnb - }, - - "Karmylla's Immersive Map Edits": { - "ID": "Karmylla.ImmersiveMapEdits", - "Default | UpdateKey": "Nexus:1149", - "~2.4 | Status": "AssumeBroken" // references deleted Content/weapons.xnb - }, - - "Secret Gardens Greenhouse": { - "ID": "jessebot.secretgardens", - "Default | UpdateKey": "Nexus:3067", - "~2.0.1 | Status": "AssumeBroken" // references deleted Content/Mine.xnb - }, - - /********* - ** Broke circa SDV 1.3 - *********/ - "Canon-Friendly Dialogue Expansion": { - "ID": "gizzymo.canonfriendlyexpansion", - "~1.1.1 | Status": "AssumeBroken" // causes a save crash on certain dates - }, - - "Everytime Submarine": { - "ID": "MustafaDemirel.EverytimeSubmarine", - "~1.0.0 | Status": "AssumeBroken" // breaks player saves if their beach bridge is fixed - }, - - "Always Scroll Map": { - "ID": "bcmpinc.AlwaysScrollMap", - "~0.6 | Status": "AssumeBroken" // breaks newer versions of bcmpinc mods (per bcmpinc's request) - }, - - "Arcade Pong": { - "ID": "Platonymous.ArcadePong", - "~1.0.2 | Status": "AssumeBroken" // broke in SMAPI 2.6-beta.16 due to reflection into SMAPI internals - }, - - "BJS Night Sounds": { - "ID": "BunnyJumps.BJSNightSounds", - "~1.0.0 | Status": "AssumeBroken" // runtime errors with Harmony 1.2.0.1 in SMAPI 2.8+ - }, - - "Craft Counter": { - "ID": "bcmpinc.CraftCounter", - "~0.6 | Status": "AssumeBroken" // breaks newer versions of bcmpinc mods (per bcmpinc's request) - }, - - "Fishing Adjust": { - "ID": "shuaiz.FishingAdjustMod", - "~2.0.1 | Status": "AssumeBroken" // Method not found: 'Void Harmony.HarmonyInstance.Patch(System.Reflection.MethodBase, Harmony.HarmonyMethod, Harmony.HarmonyMethod, Harmony.HarmonyMethod)' - }, - - "Fishing Automaton": { - "ID": "Drynwynn.FishingAutomaton", - "~1.1 | Status": "AssumeBroken" // runtime errors with Harmony 1.2.0.1 in SMAPI 2.8+ - }, - - "Fix Animal Tools": { - "ID": "bcmpinc.FixAnimalTools", - "~0.6 | Status": "AssumeBroken" // breaks newer versions of bcmpinc mods (per bcmpinc's request) - }, - - "Fix Scythe Exp": { - "ID": "bcmpinc.FixScytheExp", - "~0.3 | Status": "AssumeBroken" // broke in 1.3: Exception from HarmonyInstance "bcmpinc.FixScytheExp" [...] Bad label content in ILGenerator. - }, - - "Grass Growth": { - "ID": "bcmpinc.GrassGrowth", - "~0.6 | Status": "AssumeBroken" // breaks newer versions of bcmpinc mods (per bcmpinc's request) - }, - - "More Silo Storage": { - "ID": "OrneryWalrus.MoreSiloStorage", - "~1.0.1 | Status": "AssumeBroken" // broke in SDV 1.3 - }, - - "Movement Speed": { - "ID": "bcmpinc.MovementSpeed", - "~0.6 | Status": "AssumeBroken" // breaks newer versions of bcmpinc mods (per bcmpinc's request) - }, - - "No Added Flying Mine Monsters": { - "ID": "Drynwynn.NoAddedFlyingMineMonsters", - "~1.1 | Status": "AssumeBroken" // runtime errors with Harmony 1.2.0.1 in SMAPI 2.8+ - }, - - "Server Bookmarker": { - "ID": "Ilyaki.ServerBookmarker", - "~1.0.0 | Status": "AssumeBroken" // broke in Stardew Valley 1.3.29 (runtime errors) - }, - - "Skill Prestige: Cooking Adapter": { - "ID": "Alphablackwolf.CookingSkillPrestigeAdapter", - "FormerIDs": "20d6b8a3-b6e7-460b-a6e4-07c2b0cb6c63", // changed circa 1.1 - "MapRemoteVersions": { "1.2.3": "1.1" } // manifest not updated - }, - - "Skull Cave Saver": { - "ID": "cantorsdust.SkullCaveSaver", - "FormerIDs": "8ac06349-26f7-4394-806c-95d48fd35774 | community.SkullCaveSaver", // changed in 1.1 and 1.2.2 - "1.3-beta | Status": "AssumeBroken" // doesn't work in multiplayer, no longer maintained - }, - - "Split Screen": { - "ID": "Ilyaki.SplitScreen", - "~3.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.6-beta.16 due to reflection into SMAPI internals - }, - - "Stardew Hack": { - "ID": "bcmpinc.StardewHack", - "~0.6 | Status": "AssumeBroken" // breaks newer versions of bcmpinc mods (per bcmpinc's request) - }, - - "Stephan's Lots of Crops": { - "ID": "stephansstardewcrops", - "MapRemoteVersions": { "1.41": "1.1" }, // manifest not updated - "~1.1 | Status": "AssumeBroken" // broke in SDV 1.3 (overwrites vanilla items) - }, - - "Summit Reborn": { - "ID": "KoihimeNakamura.summitreborn", - "FormerIDs": "emissaryofinfinity.summitreborn", // changed in 1.0.2 - "~1.0.2 | Status": "AssumeBroken" // broke in SDV 1.3 (runtime errors) - }, - - "Tilled Soil Decay": { - "ID": "bcmpinc.TilledSoilDecay", - "~0.6 | Status": "AssumeBroken" // breaks newer versions of bcmpinc mods (per bcmpinc's request) - }, - - "Tree Spread": { - "ID": "bcmpinc.TreeSpread", - "~0.6 | Status": "AssumeBroken" // breaks newer versions of bcmpinc mods (per bcmpinc's request) - }, - - "Yet Another Harvest With Scythe Mod": { - "ID": "bcmpinc.HarvestWithScythe", - "~0.6 | Status": "AssumeBroken" // breaks newer versions of bcmpinc mods (per bcmpinc's request) - }, - - /********* - ** Broke circa SDV 1.2 - *********/ - "Move Faster": { - "ID": "shuaiz.MoveFasterMod", - "~1.0.1 | Status": "AssumeBroken" // doesn't do anything as of SDV 1.2.33 (bad Harmony patch?) - } - } -} diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 3ba3e6dd..8c925a58 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -59,10 +59,10 @@ namespace StardewModdingAPI internal static readonly string InternalFilesPath = Program.DllSearchPath; /// The file path for the SMAPI configuration file. - internal static string ApiConfigPath => Path.Combine(Constants.InternalFilesPath, "StardewModdingAPI.config.json"); + internal static string ApiConfigPath => Path.Combine(Constants.InternalFilesPath, "config.json"); /// The file path for the SMAPI metadata file. - internal static string ApiMetadataPath => Path.Combine(Constants.InternalFilesPath, "StardewModdingAPI.metadata.json"); + internal static string ApiMetadataPath => Path.Combine(Constants.InternalFilesPath, "metadata.json"); /// The filename prefix used for all SMAPI logs. internal static string LogNamePrefix { get; } = "SMAPI-"; diff --git a/src/SMAPI/SMAPI.config.json b/src/SMAPI/SMAPI.config.json new file mode 100644 index 00000000..c04cceee --- /dev/null +++ b/src/SMAPI/SMAPI.config.json @@ -0,0 +1,77 @@ +/* + + + +This file contains advanced configuration for SMAPI. You generally shouldn't change this file. + + + +*/ +{ + /** + * The console color theme to use. The possible values are: + * - AutoDetect: SMAPI will assume a light background on Mac, and detect the background color automatically on Linux or Windows. + * - LightBackground: use darker text colors that look better on a white or light background. + * - DarkBackground: use lighter text colors that look better on a black or dark background. + */ + "ColorScheme": "AutoDetect", + + /** + * Whether SMAPI should check for newer versions of SMAPI and mods when you load the game. If new + * versions are available, an alert will be shown in the console. This doesn't affect the load + * time even if your connection is offline or slow, because it happens in the background. + */ + "CheckForUpdates": true, + + /** + * Whether to enable features intended for mod developers. Currently this only makes TRACE-level + * messages appear in the console. + */ + "DeveloperMode": true, + + /** + * Whether to add a section to the 'mod issues' list for mods which directly use potentially + * sensitive .NET APIs like file or shell access. Note that many mods do this legitimately as + * part of their normal functionality, so these warnings are meaningless without further + * investigation. When this is commented out, it'll be true for local debug builds and false + * otherwise. + */ + //"ParanoidWarnings": true, + + /** + * Whether SMAPI should show newer beta versions as an available update. When this is commented + * out, it'll be true if the current SMAPI version is beta, and false otherwise. + */ + //"UseBetaChannel": true, + + /** + * SMAPI's GitHub project name, used to perform update checks. + */ + "GitHubProjectName": "Pathoschild/SMAPI", + + /** + * The base URL for SMAPI's web API, used to perform update checks. + * Note: the protocol will be changed to http:// on Linux/Mac due to OpenSSL issues with the + * game's bundled Mono. + */ + "WebApiBaseUrl": "https://api.smapi.io", + + /** + * Whether SMAPI should log more information about the game context. + */ + "VerboseLogging": false, + + /** + * Whether to generate a 'SMAPI-latest.metadata-dump.json' file in the logs folder with the full mod + * metadata for detected mods. This is only needed when troubleshooting some cases. + */ + "DumpMetadata": false, + + /** + * The mod IDs SMAPI should ignore when performing update checks or validating update keys. + */ + "SuppressUpdateChecks": [ + "SMAPI.ConsoleCommands", + "SMAPI.SaveBackup" + ] +} diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj index 58a71f94..d723d954 100644 --- a/src/SMAPI/SMAPI.csproj +++ b/src/SMAPI/SMAPI.csproj @@ -42,11 +42,11 @@ - + PreserveNewest - - StardewModdingAPI.metadata.json + + SMAPI.metadata.json PreserveNewest diff --git a/src/SMAPI/StardewModdingAPI.config.json b/src/SMAPI/StardewModdingAPI.config.json deleted file mode 100644 index c04cceee..00000000 --- a/src/SMAPI/StardewModdingAPI.config.json +++ /dev/null @@ -1,77 +0,0 @@ -/* - - - -This file contains advanced configuration for SMAPI. You generally shouldn't change this file. - - - -*/ -{ - /** - * The console color theme to use. The possible values are: - * - AutoDetect: SMAPI will assume a light background on Mac, and detect the background color automatically on Linux or Windows. - * - LightBackground: use darker text colors that look better on a white or light background. - * - DarkBackground: use lighter text colors that look better on a black or dark background. - */ - "ColorScheme": "AutoDetect", - - /** - * Whether SMAPI should check for newer versions of SMAPI and mods when you load the game. If new - * versions are available, an alert will be shown in the console. This doesn't affect the load - * time even if your connection is offline or slow, because it happens in the background. - */ - "CheckForUpdates": true, - - /** - * Whether to enable features intended for mod developers. Currently this only makes TRACE-level - * messages appear in the console. - */ - "DeveloperMode": true, - - /** - * Whether to add a section to the 'mod issues' list for mods which directly use potentially - * sensitive .NET APIs like file or shell access. Note that many mods do this legitimately as - * part of their normal functionality, so these warnings are meaningless without further - * investigation. When this is commented out, it'll be true for local debug builds and false - * otherwise. - */ - //"ParanoidWarnings": true, - - /** - * Whether SMAPI should show newer beta versions as an available update. When this is commented - * out, it'll be true if the current SMAPI version is beta, and false otherwise. - */ - //"UseBetaChannel": true, - - /** - * SMAPI's GitHub project name, used to perform update checks. - */ - "GitHubProjectName": "Pathoschild/SMAPI", - - /** - * The base URL for SMAPI's web API, used to perform update checks. - * Note: the protocol will be changed to http:// on Linux/Mac due to OpenSSL issues with the - * game's bundled Mono. - */ - "WebApiBaseUrl": "https://api.smapi.io", - - /** - * Whether SMAPI should log more information about the game context. - */ - "VerboseLogging": false, - - /** - * Whether to generate a 'SMAPI-latest.metadata-dump.json' file in the logs folder with the full mod - * metadata for detected mods. This is only needed when troubleshooting some cases. - */ - "DumpMetadata": false, - - /** - * The mod IDs SMAPI should ignore when performing update checks or validating update keys. - */ - "SuppressUpdateChecks": [ - "SMAPI.ConsoleCommands", - "SMAPI.SaveBackup" - ] -} -- cgit From e18ffc009d4fd56b200b0bb13b671940c9fadbc7 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 25 Apr 2019 00:50:26 -0400 Subject: update mods + mod build package for smapi-internal changes --- build/prepare-nuget-package.targets | 8 ++++---- docs/mod-build-config.md | 4 ++-- src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj | 1 + src/SMAPI.ModBuildConfig/build/smapi.targets | 14 +++++++------- src/SMAPI.ModBuildConfig/package.nuspec | 6 +++--- src/SMAPI/Constants.cs | 12 ++++++++---- 6 files changed, 25 insertions(+), 20 deletions(-) (limited to 'src') diff --git a/build/prepare-nuget-package.targets b/build/prepare-nuget-package.targets index 8894c273..cafaeda6 100644 --- a/build/prepare-nuget-package.targets +++ b/build/prepare-nuget-package.targets @@ -13,9 +13,9 @@ - - - - + + + + diff --git a/docs/mod-build-config.md b/docs/mod-build-config.md index fc63a5a2..aee81113 100644 --- a/docs/mod-build-config.md +++ b/docs/mod-build-config.md @@ -228,11 +228,11 @@ _[Game path](#game-path)_ above. ## Release notes ### Upcoming release -* Updated for Stardew Valley 1.4. +* Updated for SMAPI 3.0 and Stardew Valley 1.4. * If the project contains an `assets` folder, its contents are now included in the mod automatically. * Fixed `Newtonsoft.Json.pdb` included in release zips when Json.NET is referenced directly. * Fixed `` not working for `i18n` files. -* Dropped support for very old versions of SMAPI and Visual Studio. +* Dropped support for older versions of SMAPI and Visual Studio. ### 2.2 * Added support for SMAPI 2.8+ (still compatible with earlier versions). diff --git a/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj b/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj index f39a1555..0af8eef9 100644 --- a/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj +++ b/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj @@ -36,5 +36,6 @@ + diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets index f703477f..c8f60c65 100644 --- a/src/SMAPI.ModBuildConfig/build/smapi.targets +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -2,7 +2,7 @@ - + + - - $(DeployModFolderName) - $(DeployModZipTo) - - + $(MSBuildProjectName) $(TargetDir) True -- cgit From d5a7465b0219c3d736b1ac5aff1758ba41887fcc Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 11 Jun 2019 20:23:20 -0400 Subject: add mod build features for new csproj format --- docs/mod-build-config.md | 5 ++++- src/SMAPI.ModBuildConfig/build/smapi.targets | 7 +++++++ src/SMAPI.ModBuildConfig/package.nuspec | 5 ++++- 3 files changed, 15 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/docs/mod-build-config.md b/docs/mod-build-config.md index aee81113..22b5f4ea 100644 --- a/docs/mod-build-config.md +++ b/docs/mod-build-config.md @@ -230,8 +230,11 @@ _[Game path](#game-path)_ above. ### Upcoming release * Updated for SMAPI 3.0 and Stardew Valley 1.4. * If the project contains an `assets` folder, its contents are now included in the mod automatically. +* For projects using the new `.csproj` format: + * platform target is now set to x86 automatically to avoid mismatching platform target warnings; + * added GAC to assembly search paths to fix references to XNA Framework. * Fixed `Newtonsoft.Json.pdb` included in release zips when Json.NET is referenced directly. -* Fixed `` not working for `i18n` files. +* Fixed `<IgnoreModFilePatterns>` not working for `i18n` files. * Dropped support for older versions of SMAPI and Visual Studio. ### 2.2 diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets index 79a491e7..e9209a45 100644 --- a/src/SMAPI.ModBuildConfig/build/smapi.targets +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -13,6 +13,13 @@ + + x86 + x86 + + + $(AssemblySearchPaths);{GAC} + $(MSBuildProjectName) $(TargetDir) diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec index c6ee6503..28bcf807 100644 --- a/src/SMAPI.ModBuildConfig/package.nuspec +++ b/src/SMAPI.ModBuildConfig/package.nuspec @@ -2,7 +2,7 @@ Pathoschild.Stardew.ModBuildConfig - 3.0.0-alpha.20190426 + 3.0.0-alpha.20190611 Build package for SMAPI mods Pathoschild Pathoschild @@ -16,6 +16,9 @@ 3.0.0: - Updated for SMAPI 3.0 and Stardew Valley 1.4. - If the project contains an `assets` folder, its contents are now included in the mod automatically. + - For projects using the new `.csproj` format: + - platform target is now set to x86 automatically to avoid mismatching platform target warnings; + - added GAC to assembly search paths to fix references to XNA Framework. - Fixed `Newtonsoft.Json.pdb` included in release zips when Json.NET is referenced directly. - Fixed `<IgnoreModFilePatterns>` not working for `i18n` files. - Dropped support for older versions of SMAPI and Visual Studio. -- cgit From 93551c094139d4446ef5c4fc69bcda26e49e62bd Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 13 Jun 2019 18:19:27 -0400 Subject: add temporary backwards compatibility for SDV 1.3.36 in mod build package --- docs/mod-build-config.md | 4 ++-- src/SMAPI.ModBuildConfig/build/smapi.targets | 10 ++++++++-- src/SMAPI.ModBuildConfig/package.nuspec | 4 ++-- 3 files changed, 12 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/docs/mod-build-config.md b/docs/mod-build-config.md index 22b5f4ea..4ffc34a9 100644 --- a/docs/mod-build-config.md +++ b/docs/mod-build-config.md @@ -228,13 +228,13 @@ _[Game path](#game-path)_ above. ## Release notes ### Upcoming release -* Updated for SMAPI 3.0 and Stardew Valley 1.4. +* Updated for SMAPI 3.0 and Stardew Valley 1.4. (The beta is backwards-compatible with previous versions.) * If the project contains an `assets` folder, its contents are now included in the mod automatically. * For projects using the new `.csproj` format: * platform target is now set to x86 automatically to avoid mismatching platform target warnings; * added GAC to assembly search paths to fix references to XNA Framework. * Fixed `Newtonsoft.Json.pdb` included in release zips when Json.NET is referenced directly. -* Fixed `<IgnoreModFilePatterns>` not working for `i18n` files. +* Fixed `` not working for `i18n` files. * Dropped support for older versions of SMAPI and Visual Studio. ### 2.2 diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets index e9209a45..103e996a 100644 --- a/src/SMAPI.ModBuildConfig/build/smapi.targets +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -98,7 +98,7 @@ false true - + $(GamePath)\StardewValley.GameData.dll false true @@ -112,6 +112,9 @@ $(GamePath)\smapi-internal\SMAPI.Toolkit.CoreInterfaces.dll false true + + + $(GamePath)\smapi-internal\StardewModdingAPI.Toolkit.CoreInterfaces.dll $(GamePath)\xTile.dll @@ -142,7 +145,7 @@ false true - + $(GamePath)\StardewValley.GameData.MonoGame.dll false true @@ -156,6 +159,9 @@ $(GamePath)\smapi-internal\SMAPI.Toolkit.CoreInterfaces.dll false true + + + $(GamePath)\smapi-internal\StardewModdingAPI.Toolkit.CoreInterfaces.dll $(GamePath)\xTile.dll diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec index 28bcf807..e82bded4 100644 --- a/src/SMAPI.ModBuildConfig/package.nuspec +++ b/src/SMAPI.ModBuildConfig/package.nuspec @@ -2,7 +2,7 @@ Pathoschild.Stardew.ModBuildConfig - 3.0.0-alpha.20190611 + 3.0.0-beta.2 Build package for SMAPI mods Pathoschild Pathoschild @@ -14,7 +14,7 @@ Automates the build configuration for crossplatform Stardew Valley SMAPI mods. For SMAPI 2.11 or later. 3.0.0: - - Updated for SMAPI 3.0 and Stardew Valley 1.4. + - Updated for SMAPI 3.0 and Stardew Valley 1.4. (The beta is backwards-compatible with previous versions.) - If the project contains an `assets` folder, its contents are now included in the mod automatically. - For projects using the new `.csproj` format: - platform target is now set to x86 automatically to avoid mismatching platform target warnings; -- cgit From 31c882c8cea66bbd2805cc5aab11abbce248782c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 13 Jun 2019 19:35:51 -0400 Subject: fix map reloads not updating door warps (#643) --- docs/release-notes.md | 1 + src/SMAPI/Metadata/CoreAssetPropagator.cs | 31 ++++++++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 2cb477fd..b7cd61c6 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -19,6 +19,7 @@ These changes have not been released yet. * Fixed lag in some cases due to incorrect asset caching when playing in non-English. * Fixed lag when a mod invalidates many NPC portraits/sprites at once. * Fixed map reloads resetting tilesheet seasons. + * Fixed map reloads not updating door warps. * Fixed outdoor tilesheets being seasonalised when added to an indoor location. * For modders: diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index bb269643..186cd4ee 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Framework.Reflection; using StardewValley; @@ -14,6 +15,7 @@ using StardewValley.Objects; using StardewValley.Projectiles; using StardewValley.TerrainFeatures; using xTile; +using xTile.ObjectModel; using xTile.Tiles; namespace StardewModdingAPI.Metadata @@ -138,10 +140,37 @@ namespace StardewModdingAPI.Metadata { if (!string.IsNullOrWhiteSpace(location.mapPath.Value) && this.GetNormalisedPath(location.mapPath.Value) == key) { + // general updates location.reloadMap(); location.updateSeasonalTileSheets(); location.updateWarps(); - this.Reflection.GetField(location, nameof(location.interiorDoors)).SetValue(new InteriorDoorDictionary(location)); + + // update interior doors + location.interiorDoors.Clear(); + foreach (var entry in new InteriorDoorDictionary(location)) + location.interiorDoors.Add(entry); + + // update doors (derived from GameLocation.loadObjects) + location.doors.Clear(); + for (int x = 0; x < location.map.Layers[0].LayerWidth; ++x) + { + for (int y = 0; y < location.map.Layers[0].LayerHeight; ++y) + { + if (location.map.GetLayer("Buildings").Tiles[x, y] != null) + { + location.map.GetLayer("Buildings").Tiles[x, y].Properties.TryGetValue("Action", out PropertyValue action); + if (action != null && action.ToString().Contains("Warp")) + { + string[] strArray = action.ToString().Split(' '); + if (strArray[0] == "WarpCommunityCenter") + location.doors.Add(new Point(x, y), "CommunityCenter"); + else if ((location.Name != "Mountain" || x != 8 || y != 20) && strArray.Length > 2) + location.doors.Add(new Point(x, y), strArray[3]); + } + } + } + } + anyChanged = true; } } -- cgit From fdf221addee883f4b1ddcd42f92876f7a816c7a1 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 13 Jun 2019 23:49:05 -0400 Subject: reorganise update loop a bit (#648) --- src/SMAPI/Framework/SGame.cs | 667 ++++++++++++++++++++++--------------------- 1 file changed, 340 insertions(+), 327 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 2a3f16d4..1145207f 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -28,7 +28,6 @@ using StardewValley.Menus; using StardewValley.TerrainFeatures; using StardewValley.Tools; using xTile.Dimensions; -using xTile.Layers; using SObject = StardewValley.Object; namespace StardewModdingAPI.Framework @@ -270,15 +269,27 @@ namespace StardewModdingAPI.Framework this.DeprecationManager.PrintQueued(); /********* - ** Special cases + ** First-tick initialisation *********/ - // Perform first-tick initialisation. if (!this.IsInitialised) { this.IsInitialised = true; this.InitialiseAfterGameStarted(); } + /********* + ** Update input + *********/ + // This should *always* run, even when suppressing mod events, since the game uses + // this too. For example, doing this after mod event suppression would prevent the + // user from doing anything on the overnight shipping screen. + SInputState inputState = this.Input; + if (this.IsActive) + inputState.TrueUpdate(); + + /********* + ** Special cases + *********/ // Abort if SMAPI is exiting. if (this.CancellationToken.IsCancellationRequested) { @@ -345,6 +356,38 @@ namespace StardewModdingAPI.Framework return; } + // Raise minimal events while saving. + // While the game is writing to the save file in the background, mods can unexpectedly + // fail since they don't have exclusive access to resources (e.g. collection changed + // during enumeration errors). To avoid problems, events are not invoked while a save + // is in progress. It's safe to raise SaveEvents.BeforeSave as soon as the menu is + // opened (since the save hasn't started yet), but all other events should be suppressed. + if (Context.IsSaving) + { + // raise before-create + if (!Context.IsWorldReady && !this.IsBetweenCreateEvents) + { + this.IsBetweenCreateEvents = true; + this.Monitor.Log("Context: before save creation.", LogLevel.Trace); + events.SaveCreating.RaiseEmpty(); + } + + // raise before-save + if (Context.IsWorldReady && !this.IsBetweenSaveEvents) + { + this.IsBetweenSaveEvents = true; + this.Monitor.Log("Context: before save.", LogLevel.Trace); + events.Saving.RaiseEmpty(); + } + + // suppress non-save events + events.UnvalidatedUpdateTicking.RaiseEmpty(); + SGame.TicksElapsed++; + base.Update(gameTime); + events.UnvalidatedUpdateTicked.RaiseEmpty(); + return; + } + /********* ** Execute commands *********/ @@ -382,66 +425,6 @@ namespace StardewModdingAPI.Framework } } - /********* - ** Update input - *********/ - // This should *always* run, even when suppressing mod events, since the game uses - // this too. For example, doing this after mod event suppression would prevent the - // user from doing anything on the overnight shipping screen. - SInputState inputState = this.Input; - if (this.IsActive) - inputState.TrueUpdate(); - - /********* - ** Save events + suppress events during save - *********/ - // While the game is writing to the save file in the background, mods can unexpectedly - // fail since they don't have exclusive access to resources (e.g. collection changed - // during enumeration errors). To avoid problems, events are not invoked while a save - // is in progress. It's safe to raise SaveEvents.BeforeSave as soon as the menu is - // opened (since the save hasn't started yet), but all other events should be suppressed. - if (Context.IsSaving) - { - // raise before-create - if (!Context.IsWorldReady && !this.IsBetweenCreateEvents) - { - this.IsBetweenCreateEvents = true; - this.Monitor.Log("Context: before save creation.", LogLevel.Trace); - events.SaveCreating.RaiseEmpty(); - } - - // raise before-save - if (Context.IsWorldReady && !this.IsBetweenSaveEvents) - { - this.IsBetweenSaveEvents = true; - this.Monitor.Log("Context: before save.", LogLevel.Trace); - events.Saving.RaiseEmpty(); - } - - // suppress non-save events - events.UnvalidatedUpdateTicking.RaiseEmpty(); - SGame.TicksElapsed++; - base.Update(gameTime); - events.UnvalidatedUpdateTicked.RaiseEmpty(); - return; - } - if (this.IsBetweenCreateEvents) - { - // raise after-create - this.IsBetweenCreateEvents = false; - this.Monitor.Log($"Context: after save creation, starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); - this.OnLoadStageChanged(LoadStage.CreatedSaveFile); - events.SaveCreated.RaiseEmpty(); - } - if (this.IsBetweenSaveEvents) - { - // raise after-save - this.IsBetweenSaveEvents = false; - this.Monitor.Log($"Context: after save, starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); - events.Saved.RaiseEmpty(); - events.DayStarted.RaiseEmpty(); - } - /********* ** Update context *********/ @@ -464,340 +447,370 @@ namespace StardewModdingAPI.Framework this.Watchers.Update(); /********* - ** Locale changed events + ** Pre-update events *********/ - if (this.Watchers.LocaleWatcher.IsChanged) { - this.Monitor.Log($"Context: locale set to {this.Watchers.LocaleWatcher.CurrentValue}.", LogLevel.Trace); - - this.Watchers.LocaleWatcher.Reset(); - } - - /********* - ** Load / return-to-title events - *********/ - if (wasWorldReady && !Context.IsWorldReady) - this.OnLoadStageChanged(LoadStage.None); - else if (Context.IsWorldReady && Context.LoadStage != LoadStage.Ready) - { - // print context - string context = $"Context: loaded save '{Constants.SaveFolderName}', starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}, locale set to {this.ContentCore.Language}."; - if (Context.IsMultiplayer) + /********* + ** Save created/loaded events + *********/ + if (this.IsBetweenCreateEvents) { - int onlineCount = Game1.getOnlineFarmers().Count(); - context += $" {(Context.IsMainPlayer ? "Main player" : "Farmhand")} with {onlineCount} {(onlineCount == 1 ? "player" : "players")} online."; + // raise after-create + this.IsBetweenCreateEvents = false; + this.Monitor.Log($"Context: after save creation, starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); + this.OnLoadStageChanged(LoadStage.CreatedSaveFile); + events.SaveCreated.RaiseEmpty(); + } + if (this.IsBetweenSaveEvents) + { + // raise after-save + this.IsBetweenSaveEvents = false; + this.Monitor.Log($"Context: after save, starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); + events.Saved.RaiseEmpty(); + events.DayStarted.RaiseEmpty(); } - else - context += " Single-player."; - this.Monitor.Log(context, LogLevel.Trace); - // raise events - this.OnLoadStageChanged(LoadStage.Ready); - events.SaveLoaded.RaiseEmpty(); - events.DayStarted.RaiseEmpty(); - } + /********* + ** Locale changed events + *********/ + if (this.Watchers.LocaleWatcher.IsChanged) + { + this.Monitor.Log($"Context: locale set to {this.Watchers.LocaleWatcher.CurrentValue}.", LogLevel.Trace); - /********* - ** Window events - *********/ - // Here we depend on the game's viewport instead of listening to the Window.Resize - // event because we need to notify mods after the game handles the resize, so the - // game's metadata (like Game1.viewport) are updated. That's a bit complicated - // since the game adds & removes its own handler on the fly. - if (this.Watchers.WindowSizeWatcher.IsChanged) - { - if (this.Monitor.IsVerbose) - this.Monitor.Log($"Events: window size changed to {this.Watchers.WindowSizeWatcher.CurrentValue}.", LogLevel.Trace); + this.Watchers.LocaleWatcher.Reset(); + } - Point oldSize = this.Watchers.WindowSizeWatcher.PreviousValue; - Point newSize = this.Watchers.WindowSizeWatcher.CurrentValue; + /********* + ** Load / return-to-title events + *********/ + if (wasWorldReady && !Context.IsWorldReady) + this.OnLoadStageChanged(LoadStage.None); + else if (Context.IsWorldReady && Context.LoadStage != LoadStage.Ready) + { + // print context + string context = $"Context: loaded save '{Constants.SaveFolderName}', starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}, locale set to {this.ContentCore.Language}."; + if (Context.IsMultiplayer) + { + int onlineCount = Game1.getOnlineFarmers().Count(); + context += $" {(Context.IsMainPlayer ? "Main player" : "Farmhand")} with {onlineCount} {(onlineCount == 1 ? "player" : "players")} online."; + } + else + context += " Single-player."; + this.Monitor.Log(context, LogLevel.Trace); - events.WindowResized.Raise(new WindowResizedEventArgs(oldSize, newSize)); - this.Watchers.WindowSizeWatcher.Reset(); - } + // raise events + this.OnLoadStageChanged(LoadStage.Ready); + events.SaveLoaded.RaiseEmpty(); + events.DayStarted.RaiseEmpty(); + } - /********* - ** Input events (if window has focus) - *********/ - if (this.IsActive) - { - // raise events - bool isChatInput = Game1.IsChatting || (Context.IsMultiplayer && Context.IsWorldReady && Game1.activeClickableMenu == null && Game1.currentMinigame == null && inputState.IsAnyDown(Game1.options.chatButton)); - if (!isChatInput) + /********* + ** Window events + *********/ + // Here we depend on the game's viewport instead of listening to the Window.Resize + // event because we need to notify mods after the game handles the resize, so the + // game's metadata (like Game1.viewport) are updated. That's a bit complicated + // since the game adds & removes its own handler on the fly. + if (this.Watchers.WindowSizeWatcher.IsChanged) { - ICursorPosition cursor = this.Input.CursorPosition; + if (this.Monitor.IsVerbose) + this.Monitor.Log($"Events: window size changed to {this.Watchers.WindowSizeWatcher.CurrentValue}.", LogLevel.Trace); - // raise cursor moved event - if (this.Watchers.CursorWatcher.IsChanged) - { - if (events.CursorMoved.HasListeners()) - { - ICursorPosition was = this.Watchers.CursorWatcher.PreviousValue; - ICursorPosition now = this.Watchers.CursorWatcher.CurrentValue; - this.Watchers.CursorWatcher.Reset(); + Point oldSize = this.Watchers.WindowSizeWatcher.PreviousValue; + Point newSize = this.Watchers.WindowSizeWatcher.CurrentValue; - events.CursorMoved.Raise(new CursorMovedEventArgs(was, now)); - } - else - this.Watchers.CursorWatcher.Reset(); - } + events.WindowResized.Raise(new WindowResizedEventArgs(oldSize, newSize)); + this.Watchers.WindowSizeWatcher.Reset(); + } - // raise mouse wheel scrolled - if (this.Watchers.MouseWheelScrollWatcher.IsChanged) + /********* + ** Input events (if window has focus) + *********/ + if (this.IsActive) + { + // raise events + bool isChatInput = Game1.IsChatting || (Context.IsMultiplayer && Context.IsWorldReady && Game1.activeClickableMenu == null && Game1.currentMinigame == null && inputState.IsAnyDown(Game1.options.chatButton)); + if (!isChatInput) { - if (events.MouseWheelScrolled.HasListeners() || this.Monitor.IsVerbose) + ICursorPosition cursor = this.Input.CursorPosition; + + // raise cursor moved event + if (this.Watchers.CursorWatcher.IsChanged) { - int was = this.Watchers.MouseWheelScrollWatcher.PreviousValue; - int now = this.Watchers.MouseWheelScrollWatcher.CurrentValue; - this.Watchers.MouseWheelScrollWatcher.Reset(); + if (events.CursorMoved.HasListeners()) + { + ICursorPosition was = this.Watchers.CursorWatcher.PreviousValue; + ICursorPosition now = this.Watchers.CursorWatcher.CurrentValue; + this.Watchers.CursorWatcher.Reset(); - if (this.Monitor.IsVerbose) - this.Monitor.Log($"Events: mouse wheel scrolled to {now}.", LogLevel.Trace); - events.MouseWheelScrolled.Raise(new MouseWheelScrolledEventArgs(cursor, was, now)); + events.CursorMoved.Raise(new CursorMovedEventArgs(was, now)); + } + else + this.Watchers.CursorWatcher.Reset(); } - else - this.Watchers.MouseWheelScrollWatcher.Reset(); - } - - // raise input button events - foreach (var pair in inputState.ActiveButtons) - { - SButton button = pair.Key; - InputStatus status = pair.Value; - if (status == InputStatus.Pressed) + // raise mouse wheel scrolled + if (this.Watchers.MouseWheelScrollWatcher.IsChanged) { - if (this.Monitor.IsVerbose) - this.Monitor.Log($"Events: button {button} pressed.", LogLevel.Trace); + if (events.MouseWheelScrolled.HasListeners() || this.Monitor.IsVerbose) + { + int was = this.Watchers.MouseWheelScrollWatcher.PreviousValue; + int now = this.Watchers.MouseWheelScrollWatcher.CurrentValue; + this.Watchers.MouseWheelScrollWatcher.Reset(); - events.ButtonPressed.Raise(new ButtonPressedEventArgs(button, cursor, inputState)); + if (this.Monitor.IsVerbose) + this.Monitor.Log($"Events: mouse wheel scrolled to {now}.", LogLevel.Trace); + events.MouseWheelScrolled.Raise(new MouseWheelScrolledEventArgs(cursor, was, now)); + } + else + this.Watchers.MouseWheelScrollWatcher.Reset(); } - else if (status == InputStatus.Released) + + // raise input button events + foreach (var pair in inputState.ActiveButtons) { - if (this.Monitor.IsVerbose) - this.Monitor.Log($"Events: button {button} released.", LogLevel.Trace); + SButton button = pair.Key; + InputStatus status = pair.Value; + + if (status == InputStatus.Pressed) + { + if (this.Monitor.IsVerbose) + this.Monitor.Log($"Events: button {button} pressed.", LogLevel.Trace); - events.ButtonReleased.Raise(new ButtonReleasedEventArgs(button, cursor, inputState)); + events.ButtonPressed.Raise(new ButtonPressedEventArgs(button, cursor, inputState)); + } + else if (status == InputStatus.Released) + { + if (this.Monitor.IsVerbose) + this.Monitor.Log($"Events: button {button} released.", LogLevel.Trace); + + events.ButtonReleased.Raise(new ButtonReleasedEventArgs(button, cursor, inputState)); + } } } } - } - - /********* - ** Menu events - *********/ - if (this.Watchers.ActiveMenuWatcher.IsChanged) - { - IClickableMenu was = this.Watchers.ActiveMenuWatcher.PreviousValue; - IClickableMenu now = this.Watchers.ActiveMenuWatcher.CurrentValue; - this.Watchers.ActiveMenuWatcher.Reset(); // reset here so a mod changing the menu will be raised as a new event afterwards - if (this.Monitor.IsVerbose) - this.Monitor.Log($"Context: menu changed from {was?.GetType().FullName ?? "none"} to {now?.GetType().FullName ?? "none"}.", LogLevel.Trace); + /********* + ** Menu events + *********/ + if (this.Watchers.ActiveMenuWatcher.IsChanged) + { + IClickableMenu was = this.Watchers.ActiveMenuWatcher.PreviousValue; + IClickableMenu now = this.Watchers.ActiveMenuWatcher.CurrentValue; + this.Watchers.ActiveMenuWatcher.Reset(); // reset here so a mod changing the menu will be raised as a new event afterwards - // raise menu events - events.MenuChanged.Raise(new MenuChangedEventArgs(was, now)); - } + if (this.Monitor.IsVerbose) + this.Monitor.Log($"Context: menu changed from {was?.GetType().FullName ?? "none"} to {now?.GetType().FullName ?? "none"}.", LogLevel.Trace); - /********* - ** World & player events - *********/ - if (Context.IsWorldReady) - { - bool raiseWorldEvents = !this.Watchers.SaveIdWatcher.IsChanged; // don't report changes from unloaded => loaded + // raise menu events + events.MenuChanged.Raise(new MenuChangedEventArgs(was, now)); + } - // raise location changes - if (this.Watchers.LocationsWatcher.IsChanged) + /********* + ** World & player events + *********/ + if (Context.IsWorldReady) { - // location list changes - if (this.Watchers.LocationsWatcher.IsLocationListChanged) - { - GameLocation[] added = this.Watchers.LocationsWatcher.Added.ToArray(); - GameLocation[] removed = this.Watchers.LocationsWatcher.Removed.ToArray(); - this.Watchers.LocationsWatcher.ResetLocationList(); + bool raiseWorldEvents = !this.Watchers.SaveIdWatcher.IsChanged; // don't report changes from unloaded => loaded - if (this.Monitor.IsVerbose) + // raise location changes + if (this.Watchers.LocationsWatcher.IsChanged) + { + // location list changes + if (this.Watchers.LocationsWatcher.IsLocationListChanged) { - string addedText = added.Any() ? string.Join(", ", added.Select(p => p.Name)) : "none"; - string removedText = removed.Any() ? string.Join(", ", removed.Select(p => p.Name)) : "none"; - this.Monitor.Log($"Context: location list changed (added {addedText}; removed {removedText}).", LogLevel.Trace); - } + GameLocation[] added = this.Watchers.LocationsWatcher.Added.ToArray(); + GameLocation[] removed = this.Watchers.LocationsWatcher.Removed.ToArray(); + this.Watchers.LocationsWatcher.ResetLocationList(); - events.LocationListChanged.Raise(new LocationListChangedEventArgs(added, removed)); - } + if (this.Monitor.IsVerbose) + { + string addedText = added.Any() ? string.Join(", ", added.Select(p => p.Name)) : "none"; + string removedText = removed.Any() ? string.Join(", ", removed.Select(p => p.Name)) : "none"; + this.Monitor.Log($"Context: location list changed (added {addedText}; removed {removedText}).", LogLevel.Trace); + } - // raise location contents changed - if (raiseWorldEvents) - { - foreach (LocationTracker watcher in this.Watchers.LocationsWatcher.Locations) + events.LocationListChanged.Raise(new LocationListChangedEventArgs(added, removed)); + } + + // raise location contents changed + if (raiseWorldEvents) { - // buildings changed - if (watcher.BuildingsWatcher.IsChanged) + foreach (LocationTracker watcher in this.Watchers.LocationsWatcher.Locations) { - GameLocation location = watcher.Location; - Building[] added = watcher.BuildingsWatcher.Added.ToArray(); - Building[] removed = watcher.BuildingsWatcher.Removed.ToArray(); - watcher.BuildingsWatcher.Reset(); + // buildings changed + if (watcher.BuildingsWatcher.IsChanged) + { + GameLocation location = watcher.Location; + Building[] added = watcher.BuildingsWatcher.Added.ToArray(); + Building[] removed = watcher.BuildingsWatcher.Removed.ToArray(); + watcher.BuildingsWatcher.Reset(); - events.BuildingListChanged.Raise(new BuildingListChangedEventArgs(location, added, removed)); - } + events.BuildingListChanged.Raise(new BuildingListChangedEventArgs(location, added, removed)); + } - // debris changed - if (watcher.DebrisWatcher.IsChanged) - { - GameLocation location = watcher.Location; - Debris[] added = watcher.DebrisWatcher.Added.ToArray(); - Debris[] removed = watcher.DebrisWatcher.Removed.ToArray(); - watcher.DebrisWatcher.Reset(); + // debris changed + if (watcher.DebrisWatcher.IsChanged) + { + GameLocation location = watcher.Location; + Debris[] added = watcher.DebrisWatcher.Added.ToArray(); + Debris[] removed = watcher.DebrisWatcher.Removed.ToArray(); + watcher.DebrisWatcher.Reset(); - events.DebrisListChanged.Raise(new DebrisListChangedEventArgs(location, added, removed)); - } + events.DebrisListChanged.Raise(new DebrisListChangedEventArgs(location, added, removed)); + } - // large terrain features changed - if (watcher.LargeTerrainFeaturesWatcher.IsChanged) - { - GameLocation location = watcher.Location; - LargeTerrainFeature[] added = watcher.LargeTerrainFeaturesWatcher.Added.ToArray(); - LargeTerrainFeature[] removed = watcher.LargeTerrainFeaturesWatcher.Removed.ToArray(); - watcher.LargeTerrainFeaturesWatcher.Reset(); + // large terrain features changed + if (watcher.LargeTerrainFeaturesWatcher.IsChanged) + { + GameLocation location = watcher.Location; + LargeTerrainFeature[] added = watcher.LargeTerrainFeaturesWatcher.Added.ToArray(); + LargeTerrainFeature[] removed = watcher.LargeTerrainFeaturesWatcher.Removed.ToArray(); + watcher.LargeTerrainFeaturesWatcher.Reset(); - events.LargeTerrainFeatureListChanged.Raise(new LargeTerrainFeatureListChangedEventArgs(location, added, removed)); - } + events.LargeTerrainFeatureListChanged.Raise(new LargeTerrainFeatureListChangedEventArgs(location, added, removed)); + } - // NPCs changed - if (watcher.NpcsWatcher.IsChanged) - { - GameLocation location = watcher.Location; - NPC[] added = watcher.NpcsWatcher.Added.ToArray(); - NPC[] removed = watcher.NpcsWatcher.Removed.ToArray(); - watcher.NpcsWatcher.Reset(); + // NPCs changed + if (watcher.NpcsWatcher.IsChanged) + { + GameLocation location = watcher.Location; + NPC[] added = watcher.NpcsWatcher.Added.ToArray(); + NPC[] removed = watcher.NpcsWatcher.Removed.ToArray(); + watcher.NpcsWatcher.Reset(); - events.NpcListChanged.Raise(new NpcListChangedEventArgs(location, added, removed)); - } + events.NpcListChanged.Raise(new NpcListChangedEventArgs(location, added, removed)); + } - // objects changed - if (watcher.ObjectsWatcher.IsChanged) - { - GameLocation location = watcher.Location; - KeyValuePair[] added = watcher.ObjectsWatcher.Added.ToArray(); - KeyValuePair[] removed = watcher.ObjectsWatcher.Removed.ToArray(); - watcher.ObjectsWatcher.Reset(); + // objects changed + if (watcher.ObjectsWatcher.IsChanged) + { + GameLocation location = watcher.Location; + KeyValuePair[] added = watcher.ObjectsWatcher.Added.ToArray(); + KeyValuePair[] removed = watcher.ObjectsWatcher.Removed.ToArray(); + watcher.ObjectsWatcher.Reset(); - events.ObjectListChanged.Raise(new ObjectListChangedEventArgs(location, added, removed)); - } + events.ObjectListChanged.Raise(new ObjectListChangedEventArgs(location, added, removed)); + } - // terrain features changed - if (watcher.TerrainFeaturesWatcher.IsChanged) - { - GameLocation location = watcher.Location; - KeyValuePair[] added = watcher.TerrainFeaturesWatcher.Added.ToArray(); - KeyValuePair[] removed = watcher.TerrainFeaturesWatcher.Removed.ToArray(); - watcher.TerrainFeaturesWatcher.Reset(); + // terrain features changed + if (watcher.TerrainFeaturesWatcher.IsChanged) + { + GameLocation location = watcher.Location; + KeyValuePair[] added = watcher.TerrainFeaturesWatcher.Added.ToArray(); + KeyValuePair[] removed = watcher.TerrainFeaturesWatcher.Removed.ToArray(); + watcher.TerrainFeaturesWatcher.Reset(); - events.TerrainFeatureListChanged.Raise(new TerrainFeatureListChangedEventArgs(location, added, removed)); + events.TerrainFeatureListChanged.Raise(new TerrainFeatureListChangedEventArgs(location, added, removed)); + } } } + else + this.Watchers.LocationsWatcher.Reset(); } - else - this.Watchers.LocationsWatcher.Reset(); - } - - // raise time changed - if (raiseWorldEvents && this.Watchers.TimeWatcher.IsChanged) - { - int was = this.Watchers.TimeWatcher.PreviousValue; - int now = this.Watchers.TimeWatcher.CurrentValue; - this.Watchers.TimeWatcher.Reset(); - if (this.Monitor.IsVerbose) - this.Monitor.Log($"Events: time changed from {was} to {now}.", LogLevel.Trace); - - events.TimeChanged.Raise(new TimeChangedEventArgs(was, now)); - } - else - this.Watchers.TimeWatcher.Reset(); - - // raise player events - if (raiseWorldEvents) - { - PlayerTracker playerTracker = this.Watchers.CurrentPlayerTracker; - - // raise current location changed - if (playerTracker.TryGetNewLocation(out GameLocation newLocation)) + // raise time changed + if (raiseWorldEvents && this.Watchers.TimeWatcher.IsChanged) { + int was = this.Watchers.TimeWatcher.PreviousValue; + int now = this.Watchers.TimeWatcher.CurrentValue; + this.Watchers.TimeWatcher.Reset(); + if (this.Monitor.IsVerbose) - this.Monitor.Log($"Context: set location to {newLocation.Name}.", LogLevel.Trace); + this.Monitor.Log($"Events: time changed from {was} to {now}.", LogLevel.Trace); - GameLocation oldLocation = playerTracker.LocationWatcher.PreviousValue; - events.Warped.Raise(new WarpedEventArgs(playerTracker.Player, oldLocation, newLocation)); + events.TimeChanged.Raise(new TimeChangedEventArgs(was, now)); } + else + this.Watchers.TimeWatcher.Reset(); - // raise player leveled up a skill - foreach (KeyValuePair> pair in playerTracker.GetChangedSkills()) + // raise player events + if (raiseWorldEvents) { - if (this.Monitor.IsVerbose) - this.Monitor.Log($"Events: player skill '{pair.Key}' changed from {pair.Value.PreviousValue} to {pair.Value.CurrentValue}.", LogLevel.Trace); + PlayerTracker playerTracker = this.Watchers.CurrentPlayerTracker; - events.LevelChanged.Raise(new LevelChangedEventArgs(playerTracker.Player, pair.Key, pair.Value.PreviousValue, pair.Value.CurrentValue)); - } + // raise current location changed + if (playerTracker.TryGetNewLocation(out GameLocation newLocation)) + { + if (this.Monitor.IsVerbose) + this.Monitor.Log($"Context: set location to {newLocation.Name}.", LogLevel.Trace); - // raise player inventory changed - ItemStackChange[] changedItems = playerTracker.GetInventoryChanges().ToArray(); - if (changedItems.Any()) - { - if (this.Monitor.IsVerbose) - this.Monitor.Log("Events: player inventory changed.", LogLevel.Trace); - events.InventoryChanged.Raise(new InventoryChangedEventArgs(playerTracker.Player, changedItems)); - } + GameLocation oldLocation = playerTracker.LocationWatcher.PreviousValue; + events.Warped.Raise(new WarpedEventArgs(playerTracker.Player, oldLocation, newLocation)); + } - // raise mine level changed - if (playerTracker.TryGetNewMineLevel(out int mineLevel)) - { - if (this.Monitor.IsVerbose) - this.Monitor.Log($"Context: mine level changed to {mineLevel}.", LogLevel.Trace); + // raise player leveled up a skill + foreach (KeyValuePair> pair in playerTracker.GetChangedSkills()) + { + if (this.Monitor.IsVerbose) + this.Monitor.Log($"Events: player skill '{pair.Key}' changed from {pair.Value.PreviousValue} to {pair.Value.CurrentValue}.", LogLevel.Trace); + + events.LevelChanged.Raise(new LevelChangedEventArgs(playerTracker.Player, pair.Key, pair.Value.PreviousValue, pair.Value.CurrentValue)); + } + + // raise player inventory changed + ItemStackChange[] changedItems = playerTracker.GetInventoryChanges().ToArray(); + if (changedItems.Any()) + { + if (this.Monitor.IsVerbose) + this.Monitor.Log("Events: player inventory changed.", LogLevel.Trace); + events.InventoryChanged.Raise(new InventoryChangedEventArgs(playerTracker.Player, changedItems)); + } + + // raise mine level changed + if (playerTracker.TryGetNewMineLevel(out int mineLevel)) + { + if (this.Monitor.IsVerbose) + this.Monitor.Log($"Context: mine level changed to {mineLevel}.", LogLevel.Trace); + } } + this.Watchers.CurrentPlayerTracker?.Reset(); + + // update save ID watcher + this.Watchers.SaveIdWatcher.Reset(); + } + + /********* + ** Game update + *********/ + // game launched + bool isFirstTick = SGame.TicksElapsed == 0; + if (isFirstTick) + { + Context.IsGameLaunched = true; + events.GameLaunched.Raise(new GameLaunchedEventArgs()); } - this.Watchers.CurrentPlayerTracker?.Reset(); - } - // update save ID watcher - this.Watchers.SaveIdWatcher.Reset(); + // preloaded + if (Context.IsSaveLoaded && Context.LoadStage != LoadStage.Loaded && Context.LoadStage != LoadStage.Ready && Game1.dayOfMonth != 0) + this.OnLoadStageChanged(LoadStage.Loaded); + } /********* - ** Game update + ** Game update tick *********/ - // game launched - bool isFirstTick = SGame.TicksElapsed == 0; - if (isFirstTick) { - Context.IsGameLaunched = true; - events.GameLaunched.Raise(new GameLaunchedEventArgs()); - } - - // preloaded - if (Context.IsSaveLoaded && Context.LoadStage != LoadStage.Loaded && Context.LoadStage != LoadStage.Ready && Game1.dayOfMonth != 0) - this.OnLoadStageChanged(LoadStage.Loaded); + bool isOneSecond = SGame.TicksElapsed % 60 == 0; + events.UnvalidatedUpdateTicking.RaiseEmpty(); + events.UpdateTicking.RaiseEmpty(); + if (isOneSecond) + events.OneSecondUpdateTicking.RaiseEmpty(); + try + { + this.Input.UpdateSuppression(); + SGame.TicksElapsed++; + base.Update(gameTime); + } + catch (Exception ex) + { + this.MonitorForGame.Log($"An error occured in the base update loop: {ex.GetLogSummary()}", LogLevel.Error); + } - // update tick - bool isOneSecond = SGame.TicksElapsed % 60 == 0; - events.UnvalidatedUpdateTicking.RaiseEmpty(); - events.UpdateTicking.RaiseEmpty(); - if (isOneSecond) - events.OneSecondUpdateTicking.RaiseEmpty(); - try - { - this.Input.UpdateSuppression(); - SGame.TicksElapsed++; - base.Update(gameTime); - } - catch (Exception ex) - { - this.MonitorForGame.Log($"An error occured in the base update loop: {ex.GetLogSummary()}", LogLevel.Error); + events.UnvalidatedUpdateTicked.RaiseEmpty(); + events.UpdateTicked.RaiseEmpty(); + if (isOneSecond) + events.OneSecondUpdateTicked.RaiseEmpty(); } - events.UnvalidatedUpdateTicked.RaiseEmpty(); - events.UpdateTicked.RaiseEmpty(); - if (isOneSecond) - events.OneSecondUpdateTicked.RaiseEmpty(); /********* ** Update events -- cgit From d3209b17de4e46ee1d604aac24642af80ce855cc Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 14 Jun 2019 01:16:50 -0400 Subject: decouple updating watchers & raising event to fix some mod changes not being tracked correctly (#648) --- docs/release-notes.md | 1 + src/SMAPI/Framework/SGame.cs | 240 +++++++-------------- src/SMAPI/Framework/SnapshotDiff.cs | 43 ++++ src/SMAPI/Framework/SnapshotListDiff.cs | 58 +++++ src/SMAPI/Framework/StateTracking/PlayerTracker.cs | 35 +-- .../StateTracking/Snapshots/LocationSnapshot.cs | 59 +++++ .../StateTracking/Snapshots/PlayerSnapshot.cs | 53 +++++ .../StateTracking/Snapshots/WatcherSnapshot.cs | 66 ++++++ .../Snapshots/WorldLocationsSnapshot.cs | 52 +++++ .../StateTracking/WorldLocationsTracker.cs | 7 + 10 files changed, 413 insertions(+), 201 deletions(-) create mode 100644 src/SMAPI/Framework/SnapshotDiff.cs create mode 100644 src/SMAPI/Framework/SnapshotListDiff.cs create mode 100644 src/SMAPI/Framework/StateTracking/Snapshots/LocationSnapshot.cs create mode 100644 src/SMAPI/Framework/StateTracking/Snapshots/PlayerSnapshot.cs create mode 100644 src/SMAPI/Framework/StateTracking/Snapshots/WatcherSnapshot.cs create mode 100644 src/SMAPI/Framework/StateTracking/Snapshots/WorldLocationsSnapshot.cs (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index b7cd61c6..6d883baf 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -32,6 +32,7 @@ These changes have not been released yet. * Removed all deprecated APIs. * Removed the `Monitor.ExitGameImmediately` method. * Updated to Json.NET 12.0.1. + * Fixed issue where mod changes weren't tracked correctly for raising events in some cases. Events now reflect a frozen snapshot of the game state, and any mod changes are reflected in the next event tick. * Fixed `LoadStageChanged` event not raising correct flags in some cases when creating a new save. ## 2.11.3 diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 1145207f..7222899a 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -16,19 +16,16 @@ using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Input; using StardewModdingAPI.Framework.Networking; using StardewModdingAPI.Framework.Reflection; -using StardewModdingAPI.Framework.StateTracking; +using StardewModdingAPI.Framework.StateTracking.Snapshots; using StardewModdingAPI.Framework.Utilities; using StardewModdingAPI.Toolkit.Serialisation; using StardewValley; using StardewValley.BellsAndWhistles; -using StardewValley.Buildings; using StardewValley.Events; using StardewValley.Locations; using StardewValley.Menus; -using StardewValley.TerrainFeatures; using StardewValley.Tools; using xTile.Dimensions; -using SObject = StardewValley.Object; namespace StardewModdingAPI.Framework { @@ -93,6 +90,9 @@ namespace StardewModdingAPI.Framework /// Monitors the entire game state for changes. private WatcherCore Watchers; + /// A snapshot of the current state. + private WatcherSnapshot WatcherSnapshot = new WatcherSnapshot(); + /// Whether post-game-startup initialisation has been performed. private bool IsInitialised; @@ -443,8 +443,12 @@ namespace StardewModdingAPI.Framework /********* ** Update watchers + ** (Watchers need to be updated, checked, and reset in one go so we can detect any changes mods make in event handlers.) *********/ this.Watchers.Update(); + this.WatcherSnapshot.Update(this.Watchers); + this.Watchers.Reset(); + WatcherSnapshot state = this.WatcherSnapshot; /********* ** Pre-update events @@ -473,12 +477,8 @@ namespace StardewModdingAPI.Framework /********* ** Locale changed events *********/ - if (this.Watchers.LocaleWatcher.IsChanged) - { - this.Monitor.Log($"Context: locale set to {this.Watchers.LocaleWatcher.CurrentValue}.", LogLevel.Trace); - - this.Watchers.LocaleWatcher.Reset(); - } + if (state.Locale.IsChanged) + this.Monitor.Log($"Context: locale set to {state.Locale.New}.", LogLevel.Trace); /********* ** Load / return-to-title events @@ -511,16 +511,12 @@ namespace StardewModdingAPI.Framework // event because we need to notify mods after the game handles the resize, so the // game's metadata (like Game1.viewport) are updated. That's a bit complicated // since the game adds & removes its own handler on the fly. - if (this.Watchers.WindowSizeWatcher.IsChanged) + if (state.WindowSize.IsChanged) { if (this.Monitor.IsVerbose) - this.Monitor.Log($"Events: window size changed to {this.Watchers.WindowSizeWatcher.CurrentValue}.", LogLevel.Trace); + this.Monitor.Log($"Events: window size changed to {state.WindowSize.New}.", LogLevel.Trace); - Point oldSize = this.Watchers.WindowSizeWatcher.PreviousValue; - Point newSize = this.Watchers.WindowSizeWatcher.CurrentValue; - - events.WindowResized.Raise(new WindowResizedEventArgs(oldSize, newSize)); - this.Watchers.WindowSizeWatcher.Reset(); + events.WindowResized.Raise(new WindowResizedEventArgs(state.WindowSize.Old, state.WindowSize.New)); } /********* @@ -535,35 +531,15 @@ namespace StardewModdingAPI.Framework ICursorPosition cursor = this.Input.CursorPosition; // raise cursor moved event - if (this.Watchers.CursorWatcher.IsChanged) - { - if (events.CursorMoved.HasListeners()) - { - ICursorPosition was = this.Watchers.CursorWatcher.PreviousValue; - ICursorPosition now = this.Watchers.CursorWatcher.CurrentValue; - this.Watchers.CursorWatcher.Reset(); - - events.CursorMoved.Raise(new CursorMovedEventArgs(was, now)); - } - else - this.Watchers.CursorWatcher.Reset(); - } + if (state.Cursor.IsChanged) + events.CursorMoved.Raise(new CursorMovedEventArgs(state.Cursor.Old, state.Cursor.New)); // raise mouse wheel scrolled - if (this.Watchers.MouseWheelScrollWatcher.IsChanged) + if (state.MouseWheelScroll.IsChanged) { - if (events.MouseWheelScrolled.HasListeners() || this.Monitor.IsVerbose) - { - int was = this.Watchers.MouseWheelScrollWatcher.PreviousValue; - int now = this.Watchers.MouseWheelScrollWatcher.CurrentValue; - this.Watchers.MouseWheelScrollWatcher.Reset(); - - if (this.Monitor.IsVerbose) - this.Monitor.Log($"Events: mouse wheel scrolled to {now}.", LogLevel.Trace); - events.MouseWheelScrolled.Raise(new MouseWheelScrolledEventArgs(cursor, was, now)); - } - else - this.Watchers.MouseWheelScrollWatcher.Reset(); + if (this.Monitor.IsVerbose) + this.Monitor.Log($"Events: mouse wheel scrolled to {state.MouseWheelScroll.New}.", LogLevel.Trace); + events.MouseWheelScrolled.Raise(new MouseWheelScrolledEventArgs(cursor, state.MouseWheelScroll.Old, state.MouseWheelScroll.New)); } // raise input button events @@ -593,17 +569,13 @@ namespace StardewModdingAPI.Framework /********* ** Menu events *********/ - if (this.Watchers.ActiveMenuWatcher.IsChanged) + if (state.ActiveMenu.IsChanged) { - IClickableMenu was = this.Watchers.ActiveMenuWatcher.PreviousValue; - IClickableMenu now = this.Watchers.ActiveMenuWatcher.CurrentValue; - this.Watchers.ActiveMenuWatcher.Reset(); // reset here so a mod changing the menu will be raised as a new event afterwards - if (this.Monitor.IsVerbose) - this.Monitor.Log($"Context: menu changed from {was?.GetType().FullName ?? "none"} to {now?.GetType().FullName ?? "none"}.", LogLevel.Trace); + this.Monitor.Log($"Context: menu changed from {state.ActiveMenu.Old?.GetType().FullName ?? "none"} to {state.ActiveMenu.New?.GetType().FullName ?? "none"}.", LogLevel.Trace); // raise menu events - events.MenuChanged.Raise(new MenuChangedEventArgs(was, now)); + events.MenuChanged.Raise(new MenuChangedEventArgs(state.ActiveMenu.Old, state.ActiveMenu.New)); } /********* @@ -611,163 +583,97 @@ namespace StardewModdingAPI.Framework *********/ if (Context.IsWorldReady) { - bool raiseWorldEvents = !this.Watchers.SaveIdWatcher.IsChanged; // don't report changes from unloaded => loaded + bool raiseWorldEvents = !state.SaveID.IsChanged; // don't report changes from unloaded => loaded - // raise location changes - if (this.Watchers.LocationsWatcher.IsChanged) + // location list changes + if (state.Locations.LocationList.IsChanged && (events.LocationListChanged.HasListeners() || this.Monitor.IsVerbose)) { - // location list changes - if (this.Watchers.LocationsWatcher.IsLocationListChanged) - { - GameLocation[] added = this.Watchers.LocationsWatcher.Added.ToArray(); - GameLocation[] removed = this.Watchers.LocationsWatcher.Removed.ToArray(); - this.Watchers.LocationsWatcher.ResetLocationList(); - - if (this.Monitor.IsVerbose) - { - string addedText = added.Any() ? string.Join(", ", added.Select(p => p.Name)) : "none"; - string removedText = removed.Any() ? string.Join(", ", removed.Select(p => p.Name)) : "none"; - this.Monitor.Log($"Context: location list changed (added {addedText}; removed {removedText}).", LogLevel.Trace); - } + var added = state.Locations.LocationList.Added.ToArray(); + var removed = state.Locations.LocationList.Removed.ToArray(); - events.LocationListChanged.Raise(new LocationListChangedEventArgs(added, removed)); - } - - // raise location contents changed - if (raiseWorldEvents) + if (this.Monitor.IsVerbose) { - foreach (LocationTracker watcher in this.Watchers.LocationsWatcher.Locations) - { - // buildings changed - if (watcher.BuildingsWatcher.IsChanged) - { - GameLocation location = watcher.Location; - Building[] added = watcher.BuildingsWatcher.Added.ToArray(); - Building[] removed = watcher.BuildingsWatcher.Removed.ToArray(); - watcher.BuildingsWatcher.Reset(); - - events.BuildingListChanged.Raise(new BuildingListChangedEventArgs(location, added, removed)); - } - - // debris changed - if (watcher.DebrisWatcher.IsChanged) - { - GameLocation location = watcher.Location; - Debris[] added = watcher.DebrisWatcher.Added.ToArray(); - Debris[] removed = watcher.DebrisWatcher.Removed.ToArray(); - watcher.DebrisWatcher.Reset(); - - events.DebrisListChanged.Raise(new DebrisListChangedEventArgs(location, added, removed)); - } + string addedText = added.Any() ? string.Join(", ", added.Select(p => p.Name)) : "none"; + string removedText = removed.Any() ? string.Join(", ", removed.Select(p => p.Name)) : "none"; + this.Monitor.Log($"Context: location list changed (added {addedText}; removed {removedText}).", LogLevel.Trace); + } - // large terrain features changed - if (watcher.LargeTerrainFeaturesWatcher.IsChanged) - { - GameLocation location = watcher.Location; - LargeTerrainFeature[] added = watcher.LargeTerrainFeaturesWatcher.Added.ToArray(); - LargeTerrainFeature[] removed = watcher.LargeTerrainFeaturesWatcher.Removed.ToArray(); - watcher.LargeTerrainFeaturesWatcher.Reset(); + events.LocationListChanged.Raise(new LocationListChangedEventArgs(added, removed)); + } - events.LargeTerrainFeatureListChanged.Raise(new LargeTerrainFeatureListChangedEventArgs(location, added, removed)); - } + // raise location contents changed + if (raiseWorldEvents) + { + foreach (LocationSnapshot locState in state.Locations.Locations) + { + var location = locState.Location; - // NPCs changed - if (watcher.NpcsWatcher.IsChanged) - { - GameLocation location = watcher.Location; - NPC[] added = watcher.NpcsWatcher.Added.ToArray(); - NPC[] removed = watcher.NpcsWatcher.Removed.ToArray(); - watcher.NpcsWatcher.Reset(); + // buildings changed + if (locState.Buildings.IsChanged) + events.BuildingListChanged.Raise(new BuildingListChangedEventArgs(location, locState.Buildings.Added, locState.Buildings.Removed)); - events.NpcListChanged.Raise(new NpcListChangedEventArgs(location, added, removed)); - } + // debris changed + if (locState.Debris.IsChanged) + events.DebrisListChanged.Raise(new DebrisListChangedEventArgs(location, locState.Debris.Added, locState.Debris.Removed)); - // objects changed - if (watcher.ObjectsWatcher.IsChanged) - { - GameLocation location = watcher.Location; - KeyValuePair[] added = watcher.ObjectsWatcher.Added.ToArray(); - KeyValuePair[] removed = watcher.ObjectsWatcher.Removed.ToArray(); - watcher.ObjectsWatcher.Reset(); + // large terrain features changed + if (locState.LargeTerrainFeatures.IsChanged) + events.LargeTerrainFeatureListChanged.Raise(new LargeTerrainFeatureListChangedEventArgs(location, locState.LargeTerrainFeatures.Added, locState.LargeTerrainFeatures.Removed)); - events.ObjectListChanged.Raise(new ObjectListChangedEventArgs(location, added, removed)); - } + // NPCs changed + if (locState.Npcs.IsChanged) + events.NpcListChanged.Raise(new NpcListChangedEventArgs(location, locState.Npcs.Added, locState.Npcs.Removed)); - // terrain features changed - if (watcher.TerrainFeaturesWatcher.IsChanged) - { - GameLocation location = watcher.Location; - KeyValuePair[] added = watcher.TerrainFeaturesWatcher.Added.ToArray(); - KeyValuePair[] removed = watcher.TerrainFeaturesWatcher.Removed.ToArray(); - watcher.TerrainFeaturesWatcher.Reset(); + // objects changed + if (locState.Objects.IsChanged) + events.ObjectListChanged.Raise(new ObjectListChangedEventArgs(location, locState.Objects.Added, locState.Objects.Removed)); - events.TerrainFeatureListChanged.Raise(new TerrainFeatureListChangedEventArgs(location, added, removed)); - } - } + // terrain features changed + if (locState.TerrainFeatures.IsChanged) + events.TerrainFeatureListChanged.Raise(new TerrainFeatureListChangedEventArgs(location, locState.TerrainFeatures.Added, locState.TerrainFeatures.Removed)); } - else - this.Watchers.LocationsWatcher.Reset(); } // raise time changed - if (raiseWorldEvents && this.Watchers.TimeWatcher.IsChanged) - { - int was = this.Watchers.TimeWatcher.PreviousValue; - int now = this.Watchers.TimeWatcher.CurrentValue; - this.Watchers.TimeWatcher.Reset(); - - if (this.Monitor.IsVerbose) - this.Monitor.Log($"Events: time changed from {was} to {now}.", LogLevel.Trace); - - events.TimeChanged.Raise(new TimeChangedEventArgs(was, now)); - } - else - this.Watchers.TimeWatcher.Reset(); + if (raiseWorldEvents && state.Time.IsChanged) + events.TimeChanged.Raise(new TimeChangedEventArgs(state.Time.Old, state.Time.New)); // raise player events if (raiseWorldEvents) { - PlayerTracker playerTracker = this.Watchers.CurrentPlayerTracker; + PlayerSnapshot playerState = state.CurrentPlayer; + Farmer player = playerState.Player; // raise current location changed - if (playerTracker.TryGetNewLocation(out GameLocation newLocation)) + if (playerState.Location.IsChanged) { if (this.Monitor.IsVerbose) - this.Monitor.Log($"Context: set location to {newLocation.Name}.", LogLevel.Trace); + this.Monitor.Log($"Context: set location to {playerState.Location.New}.", LogLevel.Trace); - GameLocation oldLocation = playerTracker.LocationWatcher.PreviousValue; - events.Warped.Raise(new WarpedEventArgs(playerTracker.Player, oldLocation, newLocation)); + events.Warped.Raise(new WarpedEventArgs(player, playerState.Location.Old, playerState.Location.New)); } // raise player leveled up a skill - foreach (KeyValuePair> pair in playerTracker.GetChangedSkills()) + foreach (var pair in playerState.Skills) { + if (!pair.Value.IsChanged) + continue; + if (this.Monitor.IsVerbose) - this.Monitor.Log($"Events: player skill '{pair.Key}' changed from {pair.Value.PreviousValue} to {pair.Value.CurrentValue}.", LogLevel.Trace); + this.Monitor.Log($"Events: player skill '{pair.Key}' changed from {pair.Value.Old} to {pair.Value.New}.", LogLevel.Trace); - events.LevelChanged.Raise(new LevelChangedEventArgs(playerTracker.Player, pair.Key, pair.Value.PreviousValue, pair.Value.CurrentValue)); + events.LevelChanged.Raise(new LevelChangedEventArgs(player, pair.Key, pair.Value.Old, pair.Value.New)); } // raise player inventory changed - ItemStackChange[] changedItems = playerTracker.GetInventoryChanges().ToArray(); + ItemStackChange[] changedItems = playerState.InventoryChanges.ToArray(); if (changedItems.Any()) { if (this.Monitor.IsVerbose) this.Monitor.Log("Events: player inventory changed.", LogLevel.Trace); - events.InventoryChanged.Raise(new InventoryChangedEventArgs(playerTracker.Player, changedItems)); - } - - // raise mine level changed - if (playerTracker.TryGetNewMineLevel(out int mineLevel)) - { - if (this.Monitor.IsVerbose) - this.Monitor.Log($"Context: mine level changed to {mineLevel}.", LogLevel.Trace); + events.InventoryChanged.Raise(new InventoryChangedEventArgs(player, changedItems)); } } - this.Watchers.CurrentPlayerTracker?.Reset(); - - // update save ID watcher - this.Watchers.SaveIdWatcher.Reset(); } /********* diff --git a/src/SMAPI/Framework/SnapshotDiff.cs b/src/SMAPI/Framework/SnapshotDiff.cs new file mode 100644 index 00000000..5b6288ff --- /dev/null +++ b/src/SMAPI/Framework/SnapshotDiff.cs @@ -0,0 +1,43 @@ +using StardewModdingAPI.Framework.StateTracking; + +namespace StardewModdingAPI.Framework +{ + /// A snapshot of a tracked value. + /// The tracked value type. + internal class SnapshotDiff + { + /********* + ** Accessors + *********/ + /// Whether the value changed since the last update. + public bool IsChanged { get; private set; } + + /// The previous value. + public T Old { get; private set; } + + /// The current value. + public T New { get; private set; } + + + /********* + ** Public methods + *********/ + /// Update the snapshot. + /// Whether the value changed since the last update. + /// The previous value. + /// The current value. + public void Update(bool isChanged, T old, T now) + { + this.IsChanged = isChanged; + this.Old = old; + this.New = now; + } + + /// Update the snapshot. + /// The value watcher to snapshot. + public void Update(IValueWatcher watcher) + { + this.Update(watcher.IsChanged, watcher.PreviousValue, watcher.CurrentValue); + } + } +} diff --git a/src/SMAPI/Framework/SnapshotListDiff.cs b/src/SMAPI/Framework/SnapshotListDiff.cs new file mode 100644 index 00000000..d4d5df50 --- /dev/null +++ b/src/SMAPI/Framework/SnapshotListDiff.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using StardewModdingAPI.Framework.StateTracking; + +namespace StardewModdingAPI.Framework +{ + /// A snapshot of a tracked list. + /// The tracked list value type. + internal class SnapshotListDiff + { + /********* + ** Fields + *********/ + /// The removed values. + private readonly List RemovedImpl = new List(); + + /// The added values. + private readonly List AddedImpl = new List(); + + + /********* + ** Accessors + *********/ + /// Whether the value changed since the last update. + public bool IsChanged { get; private set; } + + /// The removed values. + public IEnumerable Removed => this.RemovedImpl; + + /// The added values. + public IEnumerable Added => this.AddedImpl; + + + /********* + ** Public methods + *********/ + /// Update the snapshot. + /// Whether the value changed since the last update. + /// The removed values. + /// The added values. + public void Update(bool isChanged, IEnumerable removed, IEnumerable added) + { + this.IsChanged = isChanged; + + this.RemovedImpl.Clear(); + this.RemovedImpl.AddRange(removed); + + this.AddedImpl.Clear(); + this.AddedImpl.AddRange(added); + } + + /// Update the snapshot. + /// The value watcher to snapshot. + public void Update(ICollectionWatcher watcher) + { + this.Update(watcher.IsChanged, watcher.Removed, watcher.Added); + } + } +} diff --git a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs index abb4fa24..6302a889 100644 --- a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs +++ b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs @@ -5,7 +5,6 @@ using StardewModdingAPI.Enums; using StardewModdingAPI.Events; using StardewModdingAPI.Framework.StateTracking.FieldWatchers; using StardewValley; -using StardewValley.Locations; using ChangeType = StardewModdingAPI.Events.ChangeType; namespace StardewModdingAPI.Framework.StateTracking @@ -38,9 +37,6 @@ namespace StardewModdingAPI.Framework.StateTracking /// The player's current location. public IValueWatcher LocationWatcher { get; } - /// The player's current mine level. - public IValueWatcher MineLevelWatcher { get; } - /// Tracks changes to the player's skill levels. public IDictionary> SkillWatchers { get; } @@ -58,7 +54,6 @@ namespace StardewModdingAPI.Framework.StateTracking // init trackers this.LocationWatcher = WatcherFactory.ForReference(this.GetCurrentLocation); - this.MineLevelWatcher = WatcherFactory.ForEquatable(() => this.LastValidLocation is MineShaft mine ? mine.mineLevel : 0); this.SkillWatchers = new Dictionary> { [SkillType.Combat] = WatcherFactory.ForNetValue(player.combatLevel), @@ -70,11 +65,7 @@ namespace StardewModdingAPI.Framework.StateTracking }; // track watchers for convenience - this.Watchers.AddRange(new IWatcher[] - { - this.LocationWatcher, - this.MineLevelWatcher - }); + this.Watchers.Add(this.LocationWatcher); this.Watchers.AddRange(this.SkillWatchers.Values); } @@ -124,30 +115,6 @@ namespace StardewModdingAPI.Framework.StateTracking } } - /// Get the player skill levels which changed. - public IEnumerable>> GetChangedSkills() - { - return this.SkillWatchers.Where(p => p.Value.IsChanged); - } - - /// Get the player's new location if it changed. - /// The player's current location. - /// Returns whether it changed. - public bool TryGetNewLocation(out GameLocation location) - { - location = this.LocationWatcher.CurrentValue; - return this.LocationWatcher.IsChanged; - } - - /// Get the player's new mine level if it changed. - /// The player's current mine level. - /// Returns whether it changed. - public bool TryGetNewMineLevel(out int mineLevel) - { - mineLevel = this.MineLevelWatcher.CurrentValue; - return this.MineLevelWatcher.IsChanged; - } - /// Stop watching the player fields and release all references. public void Dispose() { diff --git a/src/SMAPI/Framework/StateTracking/Snapshots/LocationSnapshot.cs b/src/SMAPI/Framework/StateTracking/Snapshots/LocationSnapshot.cs new file mode 100644 index 00000000..d3029540 --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/Snapshots/LocationSnapshot.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using Microsoft.Xna.Framework; +using StardewValley; +using StardewValley.Buildings; +using StardewValley.TerrainFeatures; + +namespace StardewModdingAPI.Framework.StateTracking.Snapshots +{ + /// A frozen snapshot of a tracked game location. + internal class LocationSnapshot + { + /********* + ** Accessors + *********/ + /// The tracked location. + public GameLocation Location { get; } + + /// Tracks added or removed buildings. + public SnapshotListDiff Buildings { get; } = new SnapshotListDiff(); + + /// Tracks added or removed debris. + public SnapshotListDiff Debris { get; } = new SnapshotListDiff(); + + /// Tracks added or removed large terrain features. + public SnapshotListDiff LargeTerrainFeatures { get; } = new SnapshotListDiff(); + + /// Tracks added or removed NPCs. + public SnapshotListDiff Npcs { get; } = new SnapshotListDiff(); + + /// Tracks added or removed objects. + public SnapshotListDiff> Objects { get; } = new SnapshotListDiff>(); + + /// Tracks added or removed terrain features. + public SnapshotListDiff> TerrainFeatures { get; } = new SnapshotListDiff>(); + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The tracked location. + public LocationSnapshot(GameLocation location) + { + this.Location = location; + } + + /// Update the tracked values. + /// The watcher to snapshot. + public void Update(LocationTracker watcher) + { + this.Buildings.Update(watcher.BuildingsWatcher); + this.Debris.Update(watcher.DebrisWatcher); + this.LargeTerrainFeatures.Update(watcher.LargeTerrainFeaturesWatcher); + this.Npcs.Update(watcher.NpcsWatcher); + this.Objects.Update(watcher.ObjectsWatcher); + this.TerrainFeatures.Update(watcher.TerrainFeaturesWatcher); + } + } +} diff --git a/src/SMAPI/Framework/StateTracking/Snapshots/PlayerSnapshot.cs b/src/SMAPI/Framework/StateTracking/Snapshots/PlayerSnapshot.cs new file mode 100644 index 00000000..7bcd9f82 --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/Snapshots/PlayerSnapshot.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewModdingAPI.Enums; +using StardewModdingAPI.Events; +using StardewValley; + +namespace StardewModdingAPI.Framework.StateTracking.Snapshots +{ + /// A frozen snapshot of a tracked player. + internal class PlayerSnapshot + { + /********* + ** Accessors + *********/ + /// The player being tracked. + public Farmer Player { get; } + + /// The player's current location. + public SnapshotDiff Location { get; } = new SnapshotDiff(); + + /// Tracks changes to the player's skill levels. + public IDictionary> Skills { get; } = + Enum + .GetValues(typeof(SkillType)) + .Cast() + .ToDictionary(skill => skill, skill => new SnapshotDiff()); + + /// Get a list of inventory changes. + public IEnumerable InventoryChanges { get; private set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The player being tracked. + public PlayerSnapshot(Farmer player) + { + this.Player = player; + } + + /// Update the tracked values. + /// The player watcher to snapshot. + public void Update(PlayerTracker watcher) + { + this.Location.Update(watcher.LocationWatcher); + foreach (var pair in this.Skills) + pair.Value.Update(watcher.SkillWatchers[pair.Key]); + this.InventoryChanges = watcher.GetInventoryChanges().ToArray(); + } + } +} diff --git a/src/SMAPI/Framework/StateTracking/Snapshots/WatcherSnapshot.cs b/src/SMAPI/Framework/StateTracking/Snapshots/WatcherSnapshot.cs new file mode 100644 index 00000000..cf51e040 --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/Snapshots/WatcherSnapshot.cs @@ -0,0 +1,66 @@ +using Microsoft.Xna.Framework; +using StardewValley; +using StardewValley.Menus; + +namespace StardewModdingAPI.Framework.StateTracking.Snapshots +{ + /// A frozen snapshot of the game state watchers. + internal class WatcherSnapshot + { + /********* + ** Accessors + *********/ + /// Tracks changes to the window size. + public SnapshotDiff WindowSize { get; } = new SnapshotDiff(); + + /// Tracks changes to the current player. + public PlayerSnapshot CurrentPlayer { get; private set; } + + /// Tracks changes to the time of day (in 24-hour military format). + public SnapshotDiff Time { get; } = new SnapshotDiff(); + + /// Tracks changes to the save ID. + public SnapshotDiff SaveID { get; } = new SnapshotDiff(); + + /// Tracks changes to the game's locations. + public WorldLocationsSnapshot Locations { get; } = new WorldLocationsSnapshot(); + + /// Tracks changes to . + public SnapshotDiff ActiveMenu { get; } = new SnapshotDiff(); + + /// Tracks changes to the cursor position. + public SnapshotDiff Cursor { get; } = new SnapshotDiff(); + + /// Tracks changes to the mouse wheel scroll. + public SnapshotDiff MouseWheelScroll { get; } = new SnapshotDiff(); + + /// Tracks changes to the content locale. + public SnapshotDiff Locale { get; } = new SnapshotDiff(); + + + /********* + ** Public methods + *********/ + /// Update the tracked values. + /// The watchers to snapshot. + public void Update(WatcherCore watchers) + { + // update player instance + if (watchers.CurrentPlayerTracker == null) + this.CurrentPlayer = null; + else if (watchers.CurrentPlayerTracker.Player != this.CurrentPlayer?.Player) + this.CurrentPlayer = new PlayerSnapshot(watchers.CurrentPlayerTracker.Player); + + // update snapshots + this.WindowSize.Update(watchers.WindowSizeWatcher); + this.Locale.Update(watchers.LocaleWatcher); + this.CurrentPlayer?.Update(watchers.CurrentPlayerTracker); + this.Time.Update(watchers.TimeWatcher); + this.SaveID.Update(watchers.SaveIdWatcher); + this.Locations.Update(watchers.LocationsWatcher); + this.ActiveMenu.Update(watchers.ActiveMenuWatcher); + this.Cursor.Update(watchers.CursorWatcher); + this.MouseWheelScroll.Update(watchers.MouseWheelScrollWatcher); + } + } +} diff --git a/src/SMAPI/Framework/StateTracking/Snapshots/WorldLocationsSnapshot.cs b/src/SMAPI/Framework/StateTracking/Snapshots/WorldLocationsSnapshot.cs new file mode 100644 index 00000000..73ed2d8f --- /dev/null +++ b/src/SMAPI/Framework/StateTracking/Snapshots/WorldLocationsSnapshot.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using System.Linq; +using StardewModdingAPI.Framework.StateTracking.Comparers; +using StardewValley; + +namespace StardewModdingAPI.Framework.StateTracking.Snapshots +{ + /// A frozen snapshot of the tracked game locations. + internal class WorldLocationsSnapshot + { + /********* + ** Fields + *********/ + /// A map of tracked locations. + private readonly Dictionary LocationsDict = new Dictionary(new ObjectReferenceComparer()); + + + /********* + ** Accessors + *********/ + /// Tracks changes to the location list. + public SnapshotListDiff LocationList { get; } = new SnapshotListDiff(); + + /// The tracked locations. + public IEnumerable Locations => this.LocationsDict.Values; + + + /********* + ** Public methods + *********/ + /// Update the tracked values. + /// The watcher to snapshot. + public void Update(WorldLocationsTracker watcher) + { + // update location list + this.LocationList.Update(watcher.IsLocationListChanged, watcher.Added, watcher.Removed); + + // remove missing locations + foreach (var key in this.LocationsDict.Keys.Where(key => !watcher.HasLocationTracker(key)).ToArray()) + this.LocationsDict.Remove(key); + + // update locations + foreach (LocationTracker locationWatcher in watcher.Locations) + { + if (!this.LocationsDict.TryGetValue(locationWatcher.Location, out LocationSnapshot snapshot)) + this.LocationsDict[locationWatcher.Location] = snapshot = new LocationSnapshot(locationWatcher.Location); + + snapshot.Update(locationWatcher); + } + } + } +} diff --git a/src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs b/src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs index f09c69c1..303a4f3a 100644 --- a/src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs +++ b/src/SMAPI/Framework/StateTracking/WorldLocationsTracker.cs @@ -117,6 +117,13 @@ namespace StardewModdingAPI.Framework.StateTracking watcher.Reset(); } + /// Get whether the given location is tracked. + /// The location to check. + public bool HasLocationTracker(GameLocation location) + { + return this.LocationDict.ContainsKey(location); + } + /// Stop watching the player fields and release all references. public void Dispose() { -- cgit From 059a59a7bcb560b938f0baf54575719e57962e0c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 16 Jun 2019 14:12:54 -0400 Subject: fix error when loading a mod asset through a translated content manager (#647) --- src/SMAPI/Constants.cs | 3 +++ src/SMAPI/Framework/ContentCoordinator.cs | 9 +++++---- src/SMAPI/Framework/ContentManagers/GameContentManager.cs | 2 +- src/SMAPI/Framework/ContentManagers/ModContentManager.cs | 8 ++++---- src/SMAPI/Framework/ModHelpers/ContentHelper.cs | 2 +- 5 files changed, 14 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index f58722a0..e02c9265 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -97,6 +97,9 @@ namespace StardewModdingAPI /// The game's assembly name. internal static string GameAssemblyName => Constants.Platform == Platform.Windows ? "Stardew Valley" : "StardewValley"; + /// The language code for non-translated mod assets. + internal static LocalizedContentManager.LanguageCode DefaultLanguage { get; } = LocalizedContentManager.LanguageCode.en; + /********* ** Internal methods diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index 2dfe1f1c..39bebdd1 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -42,6 +42,9 @@ namespace StardewModdingAPI.Framework /// The loaded content managers (including the ). private readonly IList ContentManagers = new List(); + /// The language code for language-agnostic mod assets. + private readonly LocalizedContentManager.LanguageCode DefaultLanguage = Constants.DefaultLanguage; + /// Whether the content coordinator has been disposed. private bool IsDisposed; @@ -172,11 +175,9 @@ namespace StardewModdingAPI.Framework /// Get a copy of an asset from a mod folder. /// The asset type. - /// The internal asset key. /// The unique name for the content manager which should load this asset. /// The internal SMAPI asset key. - /// The language code for which to load content. - public T LoadManagedAsset(string internalKey, string contentManagerID, string relativePath, LocalizedContentManager.LanguageCode language) + public T LoadManagedAsset(string contentManagerID, string relativePath) { // get content manager IContentManager contentManager = this.ContentManagers.FirstOrDefault(p => p.IsNamespaced && p.Name == contentManagerID); @@ -184,7 +185,7 @@ namespace StardewModdingAPI.Framework throw new InvalidOperationException($"The '{contentManagerID}' prefix isn't handled by any mod."); // get fresh asset - return contentManager.Load(relativePath, language, useCache: false); + return contentManager.Load(relativePath, this.DefaultLanguage, useCache: false); } /// Purge assets from the cache that match one of the interceptors. diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index ecabcaca..488ec245 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -82,7 +82,7 @@ namespace StardewModdingAPI.Framework.ContentManagers // get managed asset if (this.Coordinator.TryParseManagedAssetKey(assetName, out string contentManagerID, out string relativePath)) { - T managedAsset = this.Coordinator.LoadManagedAsset(assetName, contentManagerID, relativePath, language); + T managedAsset = this.Coordinator.LoadManagedAsset(contentManagerID, relativePath); if (useCache) this.Inject(assetName, managedAsset, language); return managedAsset; diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index 1d015138..2c73e861 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -30,7 +30,7 @@ namespace StardewModdingAPI.Framework.ContentManagers private readonly IContentManager GameContentManager; /// The language code for language-agnostic mod assets. - private const LanguageCode NoLanguage = LanguageCode.en; + private readonly LanguageCode DefaultLanguage = Constants.DefaultLanguage; /********* @@ -59,7 +59,7 @@ namespace StardewModdingAPI.Framework.ContentManagers /// The asset path relative to the loader root directory, not including the .xnb extension. public override T Load(string assetName) { - return this.Load(assetName, ModContentManager.NoLanguage, useCache: false); + return this.Load(assetName, this.DefaultLanguage, useCache: false); } /// Load an asset that has been processed by the content pipeline. @@ -90,8 +90,8 @@ namespace StardewModdingAPI.Framework.ContentManagers // disable language handling // Mod files don't support automatic translation logic, so this should never happen. - if (language != ModContentManager.NoLanguage) - throw new InvalidOperationException("Caching is not supported by the mod content manager."); + if (language != this.DefaultLanguage) + throw new InvalidOperationException("Localised assets aren't supported by the mod content manager."); // resolve managed asset key { diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs index 0be9aea5..15b164b1 100644 --- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs @@ -94,7 +94,7 @@ namespace StardewModdingAPI.Framework.ModHelpers return this.GameContentManager.Load(key, this.CurrentLocaleConstant, useCache: false); case ContentSource.ModFolder: - return this.ModContentManager.Load(key, this.CurrentLocaleConstant, useCache: false); + return this.ModContentManager.Load(key, Constants.DefaultLanguage, useCache: false); default: throw new SContentLoadException($"{this.ModName} failed loading content asset '{key}' from {source}: unknown content source '{source}'."); -- cgit From 480b307e6220e5a503f02ed857873ec8a0720e91 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 16 Jun 2019 14:18:30 -0400 Subject: include PDB files in mod builds by default --- docs/mod-build-config.md | 4 +++- src/SMAPI.ModBuildConfig/build/smapi.targets | 4 ++++ src/SMAPI.ModBuildConfig/package.nuspec | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/docs/mod-build-config.md b/docs/mod-build-config.md index 4ffc34a9..22180059 100644 --- a/docs/mod-build-config.md +++ b/docs/mod-build-config.md @@ -8,7 +8,8 @@ The package... * packages the mod into your `Mods` folder when you rebuild the code (configurable); * creates a release zip (configurable); * configures Visual Studio to enable debugging into the code when the game is running (_Windows only_); -* adds C# analyzers to warn for Stardew Valley-specific issues. +* adds C# analyzers to warn for Stardew Valley-specific issues; +* preconfigures common settings (e.g. enable line numbers in stack traces). ## Contents * [Install](#install) @@ -233,6 +234,7 @@ _[Game path](#game-path)_ above. * For projects using the new `.csproj` format: * platform target is now set to x86 automatically to avoid mismatching platform target warnings; * added GAC to assembly search paths to fix references to XNA Framework. +* Builds now include `.pdb` files by default, to enable line numbers in error stack traces. * Fixed `Newtonsoft.Json.pdb` included in release zips when Json.NET is referenced directly. * Fixed `` not working for `i18n` files. * Dropped support for older versions of SMAPI and Visual Studio. diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets index 103e996a..f797b0d1 100644 --- a/src/SMAPI.ModBuildConfig/build/smapi.targets +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -17,6 +17,10 @@ x86 x86 + + pdbonly + true + $(AssemblySearchPaths);{GAC} diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec index e82bded4..d7860ee1 100644 --- a/src/SMAPI.ModBuildConfig/package.nuspec +++ b/src/SMAPI.ModBuildConfig/package.nuspec @@ -19,6 +19,7 @@ - For projects using the new `.csproj` format: - platform target is now set to x86 automatically to avoid mismatching platform target warnings; - added GAC to assembly search paths to fix references to XNA Framework. + - Builds now include `.pdb` files by default, to enable line numbers in error stack traces. - Fixed `Newtonsoft.Json.pdb` included in release zips when Json.NET is referenced directly. - Fixed `<IgnoreModFilePatterns>` not working for `i18n` files. - Dropped support for older versions of SMAPI and Visual Studio. -- cgit From 660e8087a1885fcf48b7d34b4ec726eda0e732d1 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 16 Jun 2019 23:16:35 -0400 Subject: update for location change in SDV 1.4 (#638) --- src/SMAPI/Metadata/CoreAssetPropagator.cs | 23 ++--------------------- 1 file changed, 2 insertions(+), 21 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 186cd4ee..9e84c67f 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Framework.Reflection; using StardewValley; @@ -15,7 +14,6 @@ using StardewValley.Objects; using StardewValley.Projectiles; using StardewValley.TerrainFeatures; using xTile; -using xTile.ObjectModel; using xTile.Tiles; namespace StardewModdingAPI.Metadata @@ -150,26 +148,9 @@ namespace StardewModdingAPI.Metadata foreach (var entry in new InteriorDoorDictionary(location)) location.interiorDoors.Add(entry); - // update doors (derived from GameLocation.loadObjects) + // update doors location.doors.Clear(); - for (int x = 0; x < location.map.Layers[0].LayerWidth; ++x) - { - for (int y = 0; y < location.map.Layers[0].LayerHeight; ++y) - { - if (location.map.GetLayer("Buildings").Tiles[x, y] != null) - { - location.map.GetLayer("Buildings").Tiles[x, y].Properties.TryGetValue("Action", out PropertyValue action); - if (action != null && action.ToString().Contains("Warp")) - { - string[] strArray = action.ToString().Split(' '); - if (strArray[0] == "WarpCommunityCenter") - location.doors.Add(new Point(x, y), "CommunityCenter"); - else if ((location.Name != "Mountain" || x != 8 || y != 20) && strArray.Length > 2) - location.doors.Add(new Point(x, y), strArray[3]); - } - } - } - } + location.updateDoors(); anyChanged = true; } -- cgit From 46a0dd6236a39175e85c11eac44a710db1c07847 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 17 Jun 2019 14:49:05 -0400 Subject: move environment utility into toolkit for reuse --- build/common.targets | 5 - .../ConsoleWriting/ColorfulConsoleWriter.cs | 1 + src/SMAPI.Internal/EnvironmentUtility.cs | 112 -------------------- src/SMAPI.Internal/Platform.cs | 15 --- src/SMAPI.Internal/SMAPI.Internal.projitems | 2 - src/SMAPI.Toolkit/SMAPI.Toolkit.csproj | 1 + src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs | 113 +++++++++++++++++++++ src/SMAPI.Toolkit/Utilities/Platform.cs | 15 +++ src/SMAPI/Constants.cs | 2 +- src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 2 +- .../Framework/ModLoading/PlatformAssemblyMap.cs | 2 +- src/SMAPI/GamePlatform.cs | 2 +- src/SMAPI/Program.cs | 2 +- 13 files changed, 135 insertions(+), 139 deletions(-) delete mode 100644 src/SMAPI.Internal/EnvironmentUtility.cs delete mode 100644 src/SMAPI.Internal/Platform.cs create mode 100644 src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs create mode 100644 src/SMAPI.Toolkit/Utilities/Platform.cs (limited to 'src') diff --git a/build/common.targets b/build/common.targets index e9667bf5..50ade0f7 100644 --- a/build/common.targets +++ b/build/common.targets @@ -30,11 +30,6 @@ $(DefineConstants);SMAPI_FOR_WINDOWS - - - - - diff --git a/src/SMAPI.Internal/ConsoleWriting/ColorfulConsoleWriter.cs b/src/SMAPI.Internal/ConsoleWriting/ColorfulConsoleWriter.cs index cdc729e2..40c2d986 100644 --- a/src/SMAPI.Internal/ConsoleWriting/ColorfulConsoleWriter.cs +++ b/src/SMAPI.Internal/ConsoleWriting/ColorfulConsoleWriter.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using StardewModdingAPI.Toolkit.Utilities; namespace StardewModdingAPI.Internal.ConsoleWriting { diff --git a/src/SMAPI.Internal/EnvironmentUtility.cs b/src/SMAPI.Internal/EnvironmentUtility.cs deleted file mode 100644 index c4e4678a..00000000 --- a/src/SMAPI.Internal/EnvironmentUtility.cs +++ /dev/null @@ -1,112 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -#if SMAPI_FOR_WINDOWS -using System.Management; -#endif -using System.Runtime.InteropServices; - -namespace StardewModdingAPI.Internal -{ - /// Provides methods for fetching environment information. - internal static class EnvironmentUtility - { - /********* - ** Fields - *********/ - /// Get the OS name from the system uname command. - /// The buffer to fill with the resulting string. - [DllImport("libc")] - static extern int uname(IntPtr buffer); - - - /********* - ** Public methods - *********/ - /// Detect the current OS. - public static Platform DetectPlatform() - { - switch (Environment.OSVersion.Platform) - { - case PlatformID.MacOSX: - return Platform.Mac; - - case PlatformID.Unix: - return EnvironmentUtility.IsRunningMac() - ? Platform.Mac - : Platform.Linux; - - default: - return Platform.Windows; - } - } - - - /// Get the human-readable OS name and version. - /// The current platform. - [SuppressMessage("ReSharper", "EmptyGeneralCatchClause", Justification = "Error suppressed deliberately to fallback to default behaviour.")] - public static string GetFriendlyPlatformName(Platform platform) - { -#if SMAPI_FOR_WINDOWS - try - { - return new ManagementObjectSearcher("SELECT Caption FROM Win32_OperatingSystem") - .Get() - .Cast() - .Select(entry => entry.GetPropertyValue("Caption").ToString()) - .FirstOrDefault(); - } - catch { } -#endif - return (platform == Platform.Mac ? "MacOS " : "") + Environment.OSVersion; - } - - /// Get the name of the Stardew Valley executable. - /// The current platform. - public static string GetExecutableName(Platform platform) - { - return platform == Platform.Windows - ? "Stardew Valley.exe" - : "StardewValley.exe"; - } - - /// Get whether the platform uses Mono. - /// The current platform. - public static bool IsMono(this Platform platform) - { - return platform == Platform.Linux || platform == Platform.Mac; - } - - /********* - ** Private methods - *********/ - /// Detect whether the code is running on Mac. - /// - /// This code is derived from the Mono project (see System.Windows.Forms/System.Windows.Forms/XplatUI.cs). It detects Mac by calling the - /// uname system command and checking the response, which is always 'Darwin' for MacOS. - /// - private static bool IsRunningMac() - { - IntPtr buffer = IntPtr.Zero; - try - { - buffer = Marshal.AllocHGlobal(8192); - if (EnvironmentUtility.uname(buffer) == 0) - { - string os = Marshal.PtrToStringAnsi(buffer); - return os == "Darwin"; - } - return false; - } - catch - { - return false; // default to Linux - } - finally - { - if (buffer != IntPtr.Zero) - Marshal.FreeHGlobal(buffer); - } - } - } -} diff --git a/src/SMAPI.Internal/Platform.cs b/src/SMAPI.Internal/Platform.cs deleted file mode 100644 index 81ca5c1f..00000000 --- a/src/SMAPI.Internal/Platform.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace StardewModdingAPI.Internal -{ - /// The game's platform version. - internal enum Platform - { - /// The Linux version of the game. - Linux, - - /// The Mac version of the game. - Mac, - - /// The Windows version of the game. - Windows - } -} diff --git a/src/SMAPI.Internal/SMAPI.Internal.projitems b/src/SMAPI.Internal/SMAPI.Internal.projitems index 54b12003..1408cc46 100644 --- a/src/SMAPI.Internal/SMAPI.Internal.projitems +++ b/src/SMAPI.Internal/SMAPI.Internal.projitems @@ -10,9 +10,7 @@ - - \ No newline at end of file diff --git a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj index 2c6493da..2be84316 100644 --- a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj +++ b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj @@ -18,6 +18,7 @@ + diff --git a/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs b/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs new file mode 100644 index 00000000..4f67c00e --- /dev/null +++ b/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs @@ -0,0 +1,113 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +#if SMAPI_FOR_WINDOWS +using System.Management; +#endif +using System.Runtime.InteropServices; + +namespace StardewModdingAPI.Toolkit.Utilities +{ + /// Provides methods for fetching environment information. + public static class EnvironmentUtility + { + /********* + ** Fields + *********/ + /// Get the OS name from the system uname command. + /// The buffer to fill with the resulting string. + [DllImport("libc")] + static extern int uname(IntPtr buffer); + + + /********* + ** Public methods + *********/ + /// Detect the current OS. + public static Platform DetectPlatform() + { + switch (Environment.OSVersion.Platform) + { + case PlatformID.MacOSX: + return Platform.Mac; + + case PlatformID.Unix: + return EnvironmentUtility.IsRunningMac() + ? Platform.Mac + : Platform.Linux; + + default: + return Platform.Windows; + } + } + + + /// Get the human-readable OS name and version. + /// The current platform. + [SuppressMessage("ReSharper", "EmptyGeneralCatchClause", Justification = "Error suppressed deliberately to fallback to default behaviour.")] + public static string GetFriendlyPlatformName(Platform platform) + { +#if SMAPI_FOR_WINDOWS + try + { + return new ManagementObjectSearcher("SELECT Caption FROM Win32_OperatingSystem") + .Get() + .Cast() + .Select(entry => entry.GetPropertyValue("Caption").ToString()) + .FirstOrDefault(); + } + catch { } +#endif + return (platform == Platform.Mac ? "MacOS " : "") + Environment.OSVersion; + } + + /// Get the name of the Stardew Valley executable. + /// The current platform. + public static string GetExecutableName(Platform platform) + { + return platform == Platform.Windows + ? "Stardew Valley.exe" + : "StardewValley.exe"; + } + + /// Get whether the platform uses Mono. + /// The current platform. + public static bool IsMono(this Platform platform) + { + return platform == Platform.Linux || platform == Platform.Mac; + } + + + /********* + ** Private methods + *********/ + /// Detect whether the code is running on Mac. + /// + /// This code is derived from the Mono project (see System.Windows.Forms/System.Windows.Forms/XplatUI.cs). It detects Mac by calling the + /// uname system command and checking the response, which is always 'Darwin' for MacOS. + /// + private static bool IsRunningMac() + { + IntPtr buffer = IntPtr.Zero; + try + { + buffer = Marshal.AllocHGlobal(8192); + if (EnvironmentUtility.uname(buffer) == 0) + { + string os = Marshal.PtrToStringAnsi(buffer); + return os == "Darwin"; + } + return false; + } + catch + { + return false; // default to Linux + } + finally + { + if (buffer != IntPtr.Zero) + Marshal.FreeHGlobal(buffer); + } + } + } +} diff --git a/src/SMAPI.Toolkit/Utilities/Platform.cs b/src/SMAPI.Toolkit/Utilities/Platform.cs new file mode 100644 index 00000000..d64cbeb9 --- /dev/null +++ b/src/SMAPI.Toolkit/Utilities/Platform.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Toolkit.Utilities +{ + /// The game's platform version. + public enum Platform + { + /// The Linux version of the game. + Linux, + + /// The Mac version of the game. + Mac, + + /// The Windows version of the game. + Windows + } +} diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index e02c9265..4e24477b 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -5,7 +5,7 @@ using System.Reflection; using StardewModdingAPI.Enums; using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.ModLoading; -using StardewModdingAPI.Internal; +using StardewModdingAPI.Toolkit.Utilities; using StardewValley; namespace StardewModdingAPI diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index ca171ae1..8dfacc33 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -6,9 +6,9 @@ using System.Reflection; using Mono.Cecil; using Mono.Cecil.Cil; using StardewModdingAPI.Framework.Exceptions; -using StardewModdingAPI.Internal; using StardewModdingAPI.Metadata; using StardewModdingAPI.Toolkit.Framework.ModData; +using StardewModdingAPI.Toolkit.Utilities; namespace StardewModdingAPI.Framework.ModLoading { diff --git a/src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs b/src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs index 01460dce..d4366294 100644 --- a/src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs +++ b/src/SMAPI/Framework/ModLoading/PlatformAssemblyMap.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using Mono.Cecil; -using StardewModdingAPI.Internal; +using StardewModdingAPI.Toolkit.Utilities; namespace StardewModdingAPI.Framework.ModLoading { diff --git a/src/SMAPI/GamePlatform.cs b/src/SMAPI/GamePlatform.cs index 3bd74462..174239e0 100644 --- a/src/SMAPI/GamePlatform.cs +++ b/src/SMAPI/GamePlatform.cs @@ -1,4 +1,4 @@ -using StardewModdingAPI.Internal; +using StardewModdingAPI.Toolkit.Utilities; namespace StardewModdingAPI { diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 3a34872a..df7654cd 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -7,7 +7,7 @@ using System.Threading; #if SMAPI_FOR_WINDOWS #endif using StardewModdingAPI.Framework; -using StardewModdingAPI.Internal; +using StardewModdingAPI.Toolkit.Utilities; namespace StardewModdingAPI { -- cgit From a7544e5afb4ba7f12d248503106cc8d407c2bad1 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 17 Jun 2019 18:51:12 -0400 Subject: move game detection into toolkit for reuse --- src/SMAPI.Installer/InteractiveInstaller.cs | 106 +---------------- .../Framework/GameScanning/GameScanner.cs | 129 +++++++++++++++++++++ src/SMAPI.Toolkit/ModToolkit.cs | 9 +- src/SMAPI.Toolkit/SMAPI.Toolkit.csproj | 1 + 4 files changed, 144 insertions(+), 101 deletions(-) create mode 100644 src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs (limited to 'src') diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index c8d36b01..41400617 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -7,7 +7,6 @@ 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; @@ -37,64 +36,7 @@ namespace StardewModdingApi.Installer "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. - private IEnumerable GetDefaultInstallPaths(Platform platform) - { - switch (platform) - { - case Platform.Linux: - case Platform.Mac: - { - string home = Environment.GetEnvironmentVariable("HOME"); - // Linux - yield return $"{home}/GOG Games/Stardew Valley/game"; - yield return Directory.Exists($"{home}/.steam/steam/steamapps/common/Stardew Valley") - ? $"{home}/.steam/steam/steamapps/common/Stardew Valley" - : $"{home}/.local/share/Steam/steamapps/common/Stardew Valley"; - - // Mac - yield return "/Applications/Stardew Valley.app/Contents/MacOS"; - yield return $"{home}/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS"; - } - break; - - case Platform.Windows: - { - // Windows - 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 - { - [@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150"] = "InstallLocation", // Steam - [@"SOFTWARE\WOW6432Node\GOG.com\Games\1453375253"] = "PATH", // GOG on 64-bit Windows - }; - foreach (var pair in registryKeys) - { - string path = this.GetLocalMachineRegistryValue(pair.Key, pair.Value); - if (!string.IsNullOrWhiteSpace(path)) - yield return path; - } - - // via Steam library path - string steampath = this.GetCurrentUserRegistryValue(@"Software\Valve\Steam", "SteamPath"); - if (steampath != null) - yield return Path.Combine(steampath.Replace('/', '\\'), @"steamapps\common\Stardew Valley"); - } - break; - - default: - throw new InvalidOperationException($"Unknown platform '{platform}'."); - } - } /// Get the absolute file or folder paths to remove when uninstalling SMAPI. /// The folder for Stardew Valley and SMAPI. @@ -186,8 +128,9 @@ namespace StardewModdingApi.Installer ** Step 1: initial setup *********/ /**** - ** Get platform & set window title + ** Get basic info & set window title ****/ + ModToolkit toolkit = new ModToolkit(); Platform platform = EnvironmentUtility.DetectPlatform(); Console.Title = $"SMAPI {this.GetDisplayVersion(this.GetType().Assembly.GetName().Version)} installer on {platform} {EnvironmentUtility.GetFriendlyPlatformName(platform)}"; Console.WriteLine(); @@ -323,7 +266,7 @@ namespace StardewModdingApi.Installer ****/ // get game path this.PrintInfo("Where is your game folder?"); - DirectoryInfo installDir = this.InteractivelyGetInstallPath(platform, gamePathArg); + DirectoryInfo installDir = this.InteractivelyGetInstallPath(platform, toolkit, gamePathArg); if (installDir == null) { this.PrintError("Failed finding your game path."); @@ -489,7 +432,6 @@ namespace StardewModdingApi.Installer { this.PrintDebug("Adding bundled mods..."); - ModToolkit toolkit = new ModToolkit(); ModFolder[] targetMods = toolkit.GetModFolders(paths.ModsPath).ToArray(); foreach (ModFolder sourceMod in toolkit.GetModFolders(bundledModsDir.FullName)) { @@ -597,32 +539,6 @@ namespace StardewModdingApi.Installer } } - /// Get the value of a key in the Windows HKLM registry. - /// The full path of the registry key relative to HKLM. - /// The name of the value. - private string GetLocalMachineRegistryValue(string key, string name) - { - RegistryKey localMachine = Environment.Is64BitOperatingSystem ? RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64) : Registry.LocalMachine; - RegistryKey openKey = localMachine.OpenSubKey(key); - if (openKey == null) - return null; - using (openKey) - return (string)openKey.GetValue(name); - } - - /// Get the value of a key in the Windows HKCU registry. - /// The full path of the registry key relative to HKCU. - /// The name of the value. - private string GetCurrentUserRegistryValue(string key, string name) - { - RegistryKey currentuser = Environment.Is64BitOperatingSystem ? RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64) : Registry.CurrentUser; - RegistryKey openKey = currentuser.OpenSubKey(key); - if (openKey == null) - return null; - using (openKey) - return (string)openKey.GetValue(name); - } - /// Print a message without formatting. /// The text to print. private void PrintPlain(string text) => Console.WriteLine(text); @@ -788,8 +704,9 @@ namespace StardewModdingApi.Installer /// Interactively locate the game install path to update. /// The current platform. + /// The mod toolkit. /// The path specified as a command-line argument (if any), which should override automatic path detection. - private DirectoryInfo InteractivelyGetInstallPath(Platform platform, string specifiedPath) + private DirectoryInfo InteractivelyGetInstallPath(Platform platform, ModToolkit toolkit, string specifiedPath) { // get executable name string executableFilename = EnvironmentUtility.GetExecutableName(platform); @@ -812,18 +729,7 @@ namespace StardewModdingApi.Installer } // get installed paths - DirectoryInfo[] defaultPaths = - ( - from path in this.GetDefaultInstallPaths(platform).Distinct(StringComparer.InvariantCultureIgnoreCase) - let dir = new DirectoryInfo(path) - where dir.Exists && dir.EnumerateFiles(executableFilename).Any() - select dir - ) - .GroupBy(p => p.FullName, StringComparer.InvariantCultureIgnoreCase) // ignore duplicate paths - .Select(p => p.First()) - .ToArray(); - - // choose where to install + DirectoryInfo[] defaultPaths = toolkit.GetGameFolders().ToArray(); if (defaultPaths.Any()) { // only one path diff --git a/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs b/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs new file mode 100644 index 00000000..1755a7db --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using StardewModdingAPI.Toolkit.Utilities; +#if SMAPI_FOR_WINDOWS +using Microsoft.Win32; +#endif + +namespace StardewModdingAPI.Toolkit.Framework.GameScanning +{ + /// Finds installed game folders. + public class GameScanner + { + /********* + ** Public methods + *********/ + /// Find all valid Stardew Valley install folders. + /// This checks default game locations, and on Windows checks the Windows registry for GOG/Steam install data. A folder is considered 'valid' if it contains the Stardew Valley executable for the current OS. + public IEnumerable Scan() + { + // get unique candidate paths + Platform platform = EnvironmentUtility.DetectPlatform(); + string executableFilename = EnvironmentUtility.GetExecutableName(platform); + IEnumerable paths = this.GetDefaultInstallPaths(platform).Select(PathUtilities.NormalisePathSeparators).Distinct(StringComparer.InvariantCultureIgnoreCase); + + // get valid folders + foreach (string path in paths) + { + DirectoryInfo folder = new DirectoryInfo(path); + if (folder.Exists && folder.EnumerateFiles(executableFilename).Any()) + yield return folder; + } + } + + + /********* + ** Private methods + *********/ + /// 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. + private IEnumerable GetDefaultInstallPaths(Platform platform) + { + switch (platform) + { + case Platform.Linux: + case Platform.Mac: + { + string home = Environment.GetEnvironmentVariable("HOME"); + + // Linux + yield return $"{home}/GOG Games/Stardew Valley/game"; + yield return Directory.Exists($"{home}/.steam/steam/steamapps/common/Stardew Valley") + ? $"{home}/.steam/steam/steamapps/common/Stardew Valley" + : $"{home}/.local/share/Steam/steamapps/common/Stardew Valley"; + + // Mac + yield return "/Applications/Stardew Valley.app/Contents/MacOS"; + yield return $"{home}/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS"; + } + break; + + case Platform.Windows: + { + // Windows + 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 +#if SMAPI_FOR_WINDOWS + IDictionary registryKeys = new Dictionary + { + [@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150"] = "InstallLocation", // Steam + [@"SOFTWARE\WOW6432Node\GOG.com\Games\1453375253"] = "PATH", // GOG on 64-bit Windows + }; + foreach (var pair in registryKeys) + { + string path = this.GetLocalMachineRegistryValue(pair.Key, pair.Value); + if (!string.IsNullOrWhiteSpace(path)) + yield return path; + } + + // via Steam library path + string steampath = this.GetCurrentUserRegistryValue(@"Software\Valve\Steam", "SteamPath"); + if (steampath != null) + yield return Path.Combine(steampath.Replace('/', '\\'), @"steamapps\common\Stardew Valley"); +#endif + } + break; + + default: + throw new InvalidOperationException($"Unknown platform '{platform}'."); + } + } + +#if SMAPI_FOR_WINDOWS + /// Get the value of a key in the Windows HKLM registry. + /// The full path of the registry key relative to HKLM. + /// The name of the value. + private string GetLocalMachineRegistryValue(string key, string name) + { + RegistryKey localMachine = Environment.Is64BitOperatingSystem ? RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64) : Registry.LocalMachine; + RegistryKey openKey = localMachine.OpenSubKey(key); + if (openKey == null) + return null; + using (openKey) + return (string)openKey.GetValue(name); + } + + /// Get the value of a key in the Windows HKCU registry. + /// The full path of the registry key relative to HKCU. + /// The name of the value. + private string GetCurrentUserRegistryValue(string key, string name) + { + RegistryKey currentuser = Environment.Is64BitOperatingSystem ? RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64) : Registry.CurrentUser; + RegistryKey openKey = currentuser.OpenSubKey(key); + if (openKey == null) + return null; + using (openKey) + return (string)openKey.GetValue(name); + } +#endif + } +} diff --git a/src/SMAPI.Toolkit/ModToolkit.cs b/src/SMAPI.Toolkit/ModToolkit.cs index f1da62a2..4b026b7a 100644 --- a/src/SMAPI.Toolkit/ModToolkit.cs +++ b/src/SMAPI.Toolkit/ModToolkit.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Threading.Tasks; using Newtonsoft.Json; using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; +using StardewModdingAPI.Toolkit.Framework.GameScanning; using StardewModdingAPI.Toolkit.Framework.ModData; using StardewModdingAPI.Toolkit.Framework.ModScanning; using StardewModdingAPI.Toolkit.Serialisation; @@ -46,6 +47,13 @@ namespace StardewModdingAPI.Toolkit this.UserAgent = $"SMAPI Mod Handler Toolkit/{version}"; } + /// Find valid Stardew Valley install folders. + /// This checks default game locations, and on Windows checks the Windows registry for GOG/Steam install data. A folder is considered 'valid' if it contains the Stardew Valley executable for the current OS. + public IEnumerable GetGameFolders() + { + return new GameScanner().Scan(); + } + /// Extract mod metadata from the wiki compatibility list. public async Task GetWikiCompatibilityListAsync() { @@ -72,7 +80,6 @@ namespace StardewModdingAPI.Toolkit /// Extract information about all mods in the given folder. /// The root folder containing mods. Only the will be searched, but this field allows it to be treated as a potential mod folder of its own. /// The mod path to search. - // /// If the folder contains multiple XNB mods, treat them as subfolders of a single mod. This is useful when reading a single mod archive, as opposed to a mods folder. public IEnumerable GetModFolders(string rootPath, string modPath) { return new ModScanner(this.JsonHelper).GetModFolders(rootPath, modPath); diff --git a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj index 2be84316..53bbe1ac 100644 --- a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj +++ b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj @@ -19,6 +19,7 @@ + -- cgit From 1a8c7345c3986acd172f91716bbf04fc7192317b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 17 Jun 2019 18:51:31 -0400 Subject: add stardewvalley.targets support to toolkit --- docs/release-notes.md | 1 + .../Framework/GameScanning/GameScanner.cs | 46 ++++++++++++++++++++-- 2 files changed, 44 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 6d883baf..efac8f41 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -28,6 +28,7 @@ These changes have not been released yet. * Added `Context.IsGameLaunched` field. * Mods are now loaded much earlier in the game launch. This lets mods intercept any content asset, but the game is not fully initialised when `Entry` is called (use the `GameLaunched` event if you need to run code when the game is initialised). * Added separate `LogNetworkTraffic` option to make verbose logging less overwhelmingly verbose. + * The installer now recognises custom game paths stored in `stardewvalley.targets`, if any. * When a mod is incompatible, the trace logs now list all detected issues instead of the first one. * Removed all deprecated APIs. * Removed the `Monitor.ExitGameImmediately` method. diff --git a/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs b/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs index 1755a7db..60c7a682 100644 --- a/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs +++ b/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs @@ -2,6 +2,8 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Xml.Linq; +using System.Xml.XPath; using StardewModdingAPI.Toolkit.Utilities; #if SMAPI_FOR_WINDOWS using Microsoft.Win32; @@ -19,12 +21,18 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning /// This checks default game locations, and on Windows checks the Windows registry for GOG/Steam install data. A folder is considered 'valid' if it contains the Stardew Valley executable for the current OS. public IEnumerable Scan() { - // get unique candidate paths + // get OS info Platform platform = EnvironmentUtility.DetectPlatform(); string executableFilename = EnvironmentUtility.GetExecutableName(platform); - IEnumerable paths = this.GetDefaultInstallPaths(platform).Select(PathUtilities.NormalisePathSeparators).Distinct(StringComparer.InvariantCultureIgnoreCase); - // get valid folders + // get install paths + IEnumerable paths = this + .GetCustomInstallPaths(platform) + .Concat(this.GetDefaultInstallPaths(platform)) + .Select(PathUtilities.NormalisePathSeparators) + .Distinct(StringComparer.InvariantCultureIgnoreCase); + + // yield valid folders foreach (string path in paths) { DirectoryInfo folder = new DirectoryInfo(path); @@ -98,6 +106,38 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning } } + /// Get the custom install path from the stardewvalley.targets file in the home directory, if any. + /// The target platform. + private IEnumerable GetCustomInstallPaths(Platform platform) + { + // get home path + string homePath = Environment.GetEnvironmentVariable(platform == Platform.Windows ? "USERPROFILE" : "HOME"); + if (string.IsNullOrWhiteSpace(homePath)) + yield break; + + // get targets file + FileInfo file = new FileInfo(Path.Combine(homePath, "stardewvalley.targets")); + if (!file.Exists) + yield break; + + // parse file + XElement root; + try + { + using (FileStream stream = file.OpenRead()) + root = XElement.Load(stream); + } + catch + { + yield break; + } + + // get install path + XElement element = root.XPathSelectElement("//*[local-name() = 'GamePath']"); // can't use '//GamePath' due to the default namespace + if (!string.IsNullOrWhiteSpace(element?.Value)) + yield return element.Value.Trim(); + } + #if SMAPI_FOR_WINDOWS /// Get the value of a key in the Windows HKLM registry. /// The full path of the registry key relative to HKLM. -- cgit From 2cf2b6706f9e31775b1afe05980cfaed96ab1690 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 17 Jun 2019 18:52:03 -0400 Subject: move game detection into toolkit for reuse --- docs/mod-build-config.md | 10 ++++++++++ src/SMAPI.ModBuildConfig/build/smapi.targets | 3 ++- src/SMAPI.ModBuildConfig/package.nuspec | 1 + 3 files changed, 13 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/docs/mod-build-config.md b/docs/mod-build-config.md index 22180059..a2b121f4 100644 --- a/docs/mod-build-config.md +++ b/docs/mod-build-config.md @@ -68,6 +68,14 @@ Or only create it in release builds with this: False ``` +### Debug game +The package automatically configures Visual Studio to launch/debug the game when you launch/debug +the project. To disable that, add this above the first `
` in your `.csproj`: + +```xml +False +``` + ### Game path The package usually detects where your game is installed automatically. If it can't find your game or you have multiple installs, you can specify the path yourself. There's two ways to do that: @@ -148,6 +156,7 @@ You can use the package in non-mod projects too (e.g. unit tests or framework DL disable deploying the mod and creating a release zip: ```xml +False False False ``` @@ -235,6 +244,7 @@ _[Game path](#game-path)_ above. * platform target is now set to x86 automatically to avoid mismatching platform target warnings; * added GAC to assembly search paths to fix references to XNA Framework. * Builds now include `.pdb` files by default, to enable line numbers in error stack traces. +* You can now optionally disable game debugging config. * Fixed `Newtonsoft.Json.pdb` included in release zips when Json.NET is referenced directly. * Fixed `` not working for `i18n` files. * Dropped support for older versions of SMAPI and Visual Studio. diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets index f797b0d1..7d29da0b 100644 --- a/src/SMAPI.ModBuildConfig/build/smapi.targets +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -29,6 +29,7 @@ $(TargetDir) True True + True False @@ -129,7 +130,7 @@ - + Program $(GamePath)\StardewModdingAPI.exe $(GamePath) diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec index d7860ee1..37d7959e 100644 --- a/src/SMAPI.ModBuildConfig/package.nuspec +++ b/src/SMAPI.ModBuildConfig/package.nuspec @@ -20,6 +20,7 @@ - platform target is now set to x86 automatically to avoid mismatching platform target warnings; - added GAC to assembly search paths to fix references to XNA Framework. - Builds now include `.pdb` files by default, to enable line numbers in error stack traces. + - You can now optionally disable game debugging config. - Fixed `Newtonsoft.Json.pdb` included in release zips when Json.NET is referenced directly. - Fixed `<IgnoreModFilePatterns>` not working for `i18n` files. - Dropped support for older versions of SMAPI and Visual Studio. -- cgit From ffe5bc1d4d6ba797a76856b3c98dd853d8f90086 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 17 Jun 2019 18:52:19 -0400 Subject: remove temporary backwards-compat in mod build package --- docs/mod-build-config.md | 2 +- src/SMAPI.ModBuildConfig/build/smapi.targets | 10 ++-------- src/SMAPI.ModBuildConfig/package.nuspec | 4 ++-- 3 files changed, 5 insertions(+), 11 deletions(-) (limited to 'src') diff --git a/docs/mod-build-config.md b/docs/mod-build-config.md index a2b121f4..22c37397 100644 --- a/docs/mod-build-config.md +++ b/docs/mod-build-config.md @@ -238,7 +238,7 @@ _[Game path](#game-path)_ above. ## Release notes ### Upcoming release -* Updated for SMAPI 3.0 and Stardew Valley 1.4. (The beta is backwards-compatible with previous versions.) +* Updated for SMAPI 3.0 and Stardew Valley 1.4. * If the project contains an `assets` folder, its contents are now included in the mod automatically. * For projects using the new `.csproj` format: * platform target is now set to x86 automatically to avoid mismatching platform target warnings; diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets index 7d29da0b..c24990d5 100644 --- a/src/SMAPI.ModBuildConfig/build/smapi.targets +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -103,7 +103,7 @@ false true - + $(GamePath)\StardewValley.GameData.dll false true @@ -117,9 +117,6 @@ $(GamePath)\smapi-internal\SMAPI.Toolkit.CoreInterfaces.dll false true - - - $(GamePath)\smapi-internal\StardewModdingAPI.Toolkit.CoreInterfaces.dll $(GamePath)\xTile.dll @@ -150,7 +147,7 @@ false true - + $(GamePath)\StardewValley.GameData.MonoGame.dll false true @@ -164,9 +161,6 @@ $(GamePath)\smapi-internal\SMAPI.Toolkit.CoreInterfaces.dll false true - - - $(GamePath)\smapi-internal\StardewModdingAPI.Toolkit.CoreInterfaces.dll $(GamePath)\xTile.dll diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec index 37d7959e..976b3420 100644 --- a/src/SMAPI.ModBuildConfig/package.nuspec +++ b/src/SMAPI.ModBuildConfig/package.nuspec @@ -2,7 +2,7 @@ Pathoschild.Stardew.ModBuildConfig - 3.0.0-beta.2 + 3.0.0-beta.3 Build package for SMAPI mods Pathoschild Pathoschild @@ -14,7 +14,7 @@ Automates the build configuration for crossplatform Stardew Valley SMAPI mods. For SMAPI 2.11 or later. 3.0.0: - - Updated for SMAPI 3.0 and Stardew Valley 1.4. (The beta is backwards-compatible with previous versions.) + - Updated for SMAPI 3.0 and Stardew Valley 1.4. - If the project contains an `assets` folder, its contents are now included in the mod automatically. - For projects using the new `.csproj` format: - platform target is now set to x86 automatically to avoid mismatching platform target warnings; -- cgit From b3e4162f7cb4d855f7c36def4657209cbd18ba52 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 18 Jun 2019 23:10:11 -0400 Subject: move logic into separate .targets file file for reuse --- build/common.targets | 28 +------------ build/find-game-folder.targets | 41 ++++++++++++++++++ build/prepare-nuget-package.targets | 1 + .../SMAPI.ModBuildConfig.csproj | 1 + src/SMAPI.ModBuildConfig/build/smapi.targets | 49 ++-------------------- src/SMAPI.sln | 1 + 6 files changed, 50 insertions(+), 71 deletions(-) create mode 100644 build/find-game-folder.targets (limited to 'src') diff --git a/build/common.targets b/build/common.targets index 50ade0f7..6bf1305e 100644 --- a/build/common.targets +++ b/build/common.targets @@ -1,32 +1,8 @@ - - - + - + - - $(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 Valley - C:\Program Files (x86)\GOG Galaxy\Games\Stardew Valley - C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley - - $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\GOG.com\Games\1453375253', 'PATH', null, RegistryView.Registry32)) - $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150', 'InstallLocation', null, RegistryView.Registry64, RegistryView.Registry32)) - - $(DefineConstants);SMAPI_FOR_WINDOWS diff --git a/build/find-game-folder.targets b/build/find-game-folder.targets new file mode 100644 index 00000000..a3c3f67b --- /dev/null +++ b/build/find-game-folder.targets @@ -0,0 +1,41 @@ + + + + + + + + + + + $(HOME)/GOG Games/Stardew Valley/game + $(HOME)/.steam/steam/steamapps/common/Stardew Valley + $(HOME)/.local/share/Steam/steamapps/common/Stardew Valley + + + /Applications/Stardew Valley.app/Contents/MacOS + $(HOME)/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS + + + + + + C:\Program Files\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 Valley + C:\Program Files (x86)\GOG Galaxy\Games\Stardew Valley + C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley + + + $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\GOG.com\Games\1453375253', 'PATH', null, RegistryView.Registry32)) + $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150', 'InstallLocation', null, RegistryView.Registry64, RegistryView.Registry32)) + + + <_SteamLibraryPath>$([MSBuild]::GetRegistryValueFromView('HKEY_CURRENT_USER\SOFTWARE\Valve\Steam', 'SteamPath', null, RegistryView.Registry32)) + $(_SteamLibraryPath)\steamapps\common\Stardew Valley + + + + diff --git a/build/prepare-nuget-package.targets b/build/prepare-nuget-package.targets index cafaeda6..9c1d9c2a 100644 --- a/build/prepare-nuget-package.targets +++ b/build/prepare-nuget-package.targets @@ -11,6 +11,7 @@ + diff --git a/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj b/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj index 0af8eef9..70636937 100644 --- a/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj +++ b/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj @@ -15,6 +15,7 @@ + mod-build-config.md diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets index c24990d5..10ef73af 100644 --- a/src/SMAPI.ModBuildConfig/build/smapi.targets +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -1,17 +1,12 @@ - + + + - - - - - x86 @@ -33,42 +28,6 @@ False - - - - - - $(HOME)/GOG Games/Stardew Valley/game - $(HOME)/.steam/steam/steamapps/common/Stardew Valley - $(HOME)/.local/share/Steam/steamapps/common/Stardew Valley - - - /Applications/Stardew Valley.app/Contents/MacOS - $(HOME)/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS - - - - - - C:\Program Files\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 Valley - C:\Program Files (x86)\GOG Galaxy\Games\Stardew Valley - C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley - - - $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\GOG.com\Games\1453375253', 'PATH', null, RegistryView.Registry32)) - $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150', 'InstallLocation', null, RegistryView.Registry64, RegistryView.Registry32)) - - - <_SteamLibraryPath>$([MSBuild]::GetRegistryValueFromView('HKEY_CURRENT_USER\SOFTWARE\Valve\Steam', 'SteamPath', null, RegistryView.Registry32)) - $(_SteamLibraryPath)\steamapps\common\Stardew Valley - - - - + + Stardew Valley + StardewValley + diff --git a/docs/mod-build-config.md b/docs/mod-build-config.md index 0150c65d..5b39e7aa 100644 --- a/docs/mod-build-config.md +++ b/docs/mod-build-config.md @@ -1,4 +1,4 @@ -The **mod build package** is an open-source NuGet package which automates the MSBuild configuration +The **mod build package** is an open-source NuGet package which automates the MSBuild configuration for SMAPI mods and related tools. The package is fully compatible with Linux, Mac, and Windows. ## Contents @@ -30,8 +30,14 @@ works by editing your mod's `.csproj` file, and adding the given properties betw ### Detect game path The package finds your game folder by scanning the default install paths and Windows registry. It -adds a `$(GamePath)` variable with the detected path for use in `.csproj` file (e.g. to load a -custom game DLL). If this doesn't work automatically, see [_set the game path_](#set-the-game-path). +adds two MSBuild properties for use in your `.csproj` file if needed: + +property | description +-------- | ----------- +`$(GamePath)` | The absolute path to the detected game folder. +`$(GameExecutableName)` | The game's executable name for the current OS (`Stardew Valley` on Windows, or `StardewValley` on Linux/Mac). + +If you get a build error saying it can't find your game, see [_set the game path_](#set-the-game-path). ### Add assembly references The package adds assembly references to SMAPI, Stardew Valley, xTile, and MonoGame (Linux/Mac) or XNA @@ -256,12 +262,13 @@ If you need to copy the referenced DLLs into your build output, add this too: ## Release notes ### Upcoming release * Updated for SMAPI 3.0 and Stardew Valley 1.4. -* If the project contains an `assets` folder, its contents are now included in the mod automatically. -* For projects using the new `.csproj` format: +* Added automatic support for `assets` folders. +* Added `$(GameExecutableName)` MSBuild variable. +* Added support for projects using the simplified `.csproj` format: * platform target is now set to x86 automatically to avoid mismatching platform target warnings; * added GAC to assembly search paths to fix references to XNA Framework. -* Builds now include `.pdb` files by default, to enable line numbers in error stack traces. -* You can now optionally disable game debugging config. +* Added option to disable game debugging config. +* Added `.pdb` files to builds by default (to enable line numbers in error stack traces). * Fixed `Newtonsoft.Json.pdb` included in release zips when Json.NET is referenced directly. * Fixed `` not working for `i18n` files. * Dropped support for older versions of SMAPI and Visual Studio. diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec index 976b3420..60f14efe 100644 --- a/src/SMAPI.ModBuildConfig/package.nuspec +++ b/src/SMAPI.ModBuildConfig/package.nuspec @@ -2,7 +2,7 @@ Pathoschild.Stardew.ModBuildConfig - 3.0.0-beta.3 + 3.0.0-beta.4 Build package for SMAPI mods Pathoschild Pathoschild @@ -15,12 +15,13 @@ 3.0.0: - Updated for SMAPI 3.0 and Stardew Valley 1.4. - - If the project contains an `assets` folder, its contents are now included in the mod automatically. - - For projects using the new `.csproj` format: + - Added automatic support for `assets` folders. + - Added `$(GameExecutableName)` MSBuild variable. + - Added support for projects using the simplified `.csproj` format: - platform target is now set to x86 automatically to avoid mismatching platform target warnings; - added GAC to assembly search paths to fix references to XNA Framework. - - Builds now include `.pdb` files by default, to enable line numbers in error stack traces. - - You can now optionally disable game debugging config. + - Added option to disable game debugging config. + - Added `.pdb` files to builds by default (to enable line numbers in error stack traces). - Fixed `Newtonsoft.Json.pdb` included in release zips when Json.NET is referenced directly. - Fixed `<IgnoreModFilePatterns>` not working for `i18n` files. - Dropped support for older versions of SMAPI and Visual Studio. -- cgit From 904c39eb7286df6ad3098dfdcfbf09cb282141e4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 18 Jun 2019 23:59:30 -0400 Subject: move assembly references out of common.targets --- build/common.targets | 71 +--------------------- .../SMAPI.Mods.ConsoleCommands.csproj | 49 +++++++++++++++ .../SMAPI.Mods.SaveBackup.csproj | 7 +++ src/SMAPI.Tests/SMAPI.Tests.csproj | 7 +++ src/SMAPI/SMAPI.csproj | 62 +++++++++++++++++++ 5 files changed, 126 insertions(+), 70 deletions(-) (limited to 'src') diff --git a/build/common.targets b/build/common.targets index 6bf1305e..abcf2d87 100644 --- a/build/common.targets +++ b/build/common.targets @@ -3,79 +3,10 @@ + $(AssemblySearchPaths);{GAC} $(DefineConstants);SMAPI_FOR_WINDOWS - - - - - - - $(AssemblySearchPaths);{GAC} - - - - - $(GamePath)\Stardew Valley.exe - False - - - $(GamePath)\StardewValley.GameData.dll - False - - - $(GamePath)\Netcode.dll - False - - - False - - - False - - - False - - - False - - - - - - - $(GamePath)\StardewValley.exe - False - - - $(GamePath)\StardewValley.GameData.MonoGame.dll - False - - - $(GamePath)\MonoGame.Framework.dll - False - - - - - - - $(GamePath)\GalaxyCSharp.dll - False - - - $(GamePath)\Lidgren.Network.dll - False - - - $(GamePath)\xTile.dll - False - - - - - diff --git a/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj b/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj index 13f92443..6cceb22d 100644 --- a/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj +++ b/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj @@ -17,6 +17,55 @@ + + + $(GamePath)\$(GameExecutableName).exe + False + + + + + + + + + $(GamePath)\StardewValley.GameData.dll + False + + + $(GamePath)\Netcode.dll + False + + + False + + + False + + + False + + + False + + + + + + + + + $(GamePath)\StardewValley.GameData.MonoGame.dll + False + + + $(GamePath)\MonoGame.Framework.dll + False + + + + + Properties\GlobalAssemblyInfo.cs diff --git a/src/SMAPI.Mods.SaveBackup/SMAPI.Mods.SaveBackup.csproj b/src/SMAPI.Mods.SaveBackup/SMAPI.Mods.SaveBackup.csproj index 9375cb64..d2e41e77 100644 --- a/src/SMAPI.Mods.SaveBackup/SMAPI.Mods.SaveBackup.csproj +++ b/src/SMAPI.Mods.SaveBackup/SMAPI.Mods.SaveBackup.csproj @@ -17,6 +17,13 @@ + + + $(GamePath)\$(GameExecutableName).exe + False + + + Properties\GlobalAssemblyInfo.cs diff --git a/src/SMAPI.Tests/SMAPI.Tests.csproj b/src/SMAPI.Tests/SMAPI.Tests.csproj index 1c414127..2f632a49 100644 --- a/src/SMAPI.Tests/SMAPI.Tests.csproj +++ b/src/SMAPI.Tests/SMAPI.Tests.csproj @@ -25,6 +25,13 @@ + + + $(GamePath)\$(GameExecutableName).exe + True + + + Properties\GlobalAssemblyInfo.cs diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj index d723d954..8b7d6277 100644 --- a/src/SMAPI/SMAPI.csproj +++ b/src/SMAPI/SMAPI.csproj @@ -23,6 +23,10 @@ + + $(GamePath)\$(GameExecutableName).exe + False + True @@ -32,6 +36,64 @@ + + + + + + $(GamePath)\StardewValley.GameData.dll + False + + + $(GamePath)\Netcode.dll + False + + + False + + + False + + + False + + + False + + + + + + + + + $(GamePath)\StardewValley.GameData.MonoGame.dll + False + + + $(GamePath)\MonoGame.Framework.dll + False + + + + + + + + + $(GamePath)\GalaxyCSharp.dll + False + + + $(GamePath)\Lidgren.Network.dll + False + + + $(GamePath)\xTile.dll + False + + + -- cgit From 3f28abe2c2f0f7119e168b55a63e6384c4ca92df Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 19 Jun 2019 00:06:40 -0400 Subject: update game-not-found error messages --- build/common.targets | 6 ++++-- src/SMAPI.ModBuildConfig/build/smapi.targets | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/build/common.targets b/build/common.targets index abcf2d87..804f1ed3 100644 --- a/build/common.targets +++ b/build/common.targets @@ -1,7 +1,8 @@ + - + $(AssemblySearchPaths);{GAC} $(DefineConstants);SMAPI_FOR_WINDOWS @@ -9,7 +10,7 @@ - + @@ -57,4 +58,5 @@ + diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets index 10ef73af..7ee7aa21 100644 --- a/src/SMAPI.ModBuildConfig/build/smapi.targets +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -138,7 +138,7 @@ - + -- cgit From a07e2de9a031f3d5e79202eeea726a65065f1481 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 19 Jun 2019 13:46:10 -0400 Subject: keep true/false capitalisation consistent in mod build package --- docs/mod-build-config.md | 16 ++++++++-------- src/SMAPI.ModBuildConfig/build/smapi.targets | 10 +++++----- 2 files changed, 13 insertions(+), 13 deletions(-) (limited to 'src') diff --git a/docs/mod-build-config.md b/docs/mod-build-config.md index 5b39e7aa..87e0cf30 100644 --- a/docs/mod-build-config.md +++ b/docs/mod-build-config.md @@ -46,7 +46,7 @@ Framework (Windows). It automatically adjusts depending on which OS you're compi The assemblies aren't copied to the build output, since mods loaded by SMAPI won't need them. For non-mod projects like unit tests, you can set this property: ```xml -True +true ``` ### Copy files into the `Mods` folder and create release zip @@ -82,7 +82,7 @@ You can change the folder name: Or disable deploying the files: ```xml -False +false ``` @@ -99,7 +99,7 @@ You can change the folder path where the zip is created: Or disable zip creation: ```xml -False +false ``` @@ -117,7 +117,7 @@ This is disabled on Linux/Mac due to limitations with the Mono wrapper. To disable game debugging (only needed for some non-mod projects): ```xml -False +false ``` ### Preconfigure common settings @@ -249,14 +249,14 @@ You can use the package in non-mod projects too (e.g. unit tests or framework DL the mod-related package features: ```xml -False -False -False +false +false +false ``` If you need to copy the referenced DLLs into your build output, add this too: ```xml -True +true ``` ## Release notes diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets index 7ee7aa21..1f512400 100644 --- a/src/SMAPI.ModBuildConfig/build/smapi.targets +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -22,10 +22,10 @@ $(MSBuildProjectName) $(TargetDir) - True - True - True - False + true + true + true + false @@ -86,7 +86,7 @@ - + Program $(GamePath)\StardewModdingAPI.exe $(GamePath) -- cgit From a54db1b33019645f862b0552ad2cd0700074c592 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 19 Jun 2019 13:47:05 -0400 Subject: add optional Harmony reference to mod build package --- docs/mod-build-config.md | 7 +++++++ src/SMAPI.ModBuildConfig/build/smapi.targets | 8 ++++++++ src/SMAPI.ModBuildConfig/package.nuspec | 1 + 3 files changed, 16 insertions(+) (limited to 'src') diff --git a/docs/mod-build-config.md b/docs/mod-build-config.md index 87e0cf30..9121c175 100644 --- a/docs/mod-build-config.md +++ b/docs/mod-build-config.md @@ -49,6 +49,12 @@ non-mod projects like unit tests, you can set this property: true ``` +If your mod uses [Harmony](https://github.com/pardeike/Harmony) (not recommended for most mods), +the package can add a reference to SMAPI's Harmony DLL for you: +```xml +true +``` + ### Copy files into the `Mods` folder and create release zip
Files considered part of your mod
@@ -269,6 +275,7 @@ If you need to copy the referenced DLLs into your build output, add this too: * added GAC to assembly search paths to fix references to XNA Framework. * Added option to disable game debugging config. * Added `.pdb` files to builds by default (to enable line numbers in error stack traces). +* Added optional Harmony reference. * Fixed `Newtonsoft.Json.pdb` included in release zips when Json.NET is referenced directly. * Fixed `` not working for `i18n` files. * Dropped support for older versions of SMAPI and Visual Studio. diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets index 1f512400..0daf40a1 100644 --- a/src/SMAPI.ModBuildConfig/build/smapi.targets +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -24,6 +24,7 @@ $(TargetDir) true true + false true false @@ -32,6 +33,13 @@ + + + $(GamePath)\smapi-internal\0Harmony.dll + false + true + + diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec index 60f14efe..524cadfd 100644 --- a/src/SMAPI.ModBuildConfig/package.nuspec +++ b/src/SMAPI.ModBuildConfig/package.nuspec @@ -22,6 +22,7 @@ - added GAC to assembly search paths to fix references to XNA Framework. - Added option to disable game debugging config. - Added `.pdb` files to builds by default (to enable line numbers in error stack traces). + - Added optional Harmony reference. - Fixed `Newtonsoft.Json.pdb` included in release zips when Json.NET is referenced directly. - Fixed `<IgnoreModFilePatterns>` not working for `i18n` files. - Dropped support for older versions of SMAPI and Visual Studio. -- cgit From 62d0b3b32228e0c0e8b999578a147a9dd217fb06 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 19 Jun 2019 14:22:16 -0400 Subject: reduce duplication in mod build package --- src/SMAPI.ModBuildConfig/build/smapi.targets | 175 +++++++++++---------------- 1 file changed, 69 insertions(+), 106 deletions(-) (limited to 'src') diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets index 0daf40a1..51432d96 100644 --- a/src/SMAPI.ModBuildConfig/build/smapi.targets +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -26,133 +26,96 @@ true false true - false + false + + + + + Program + $(GamePath)\StardewModdingAPI.exe + $(GamePath) + + + $(GamePath)\$(GameExecutableName).exe + $(CopyModReferencesToBuildOutput) + + + $(GamePath)\StardewModdingAPI.exe + $(CopyModReferencesToBuildOutput) + + + $(GamePath)\smapi-internal\SMAPI.Toolkit.CoreInterfaces.dll + $(CopyModReferencesToBuildOutput) + + + $(GamePath)\xTile.dll + $(CopyModReferencesToBuildOutput) + $(GamePath)\smapi-internal\0Harmony.dll - false - true + $(CopyModReferencesToBuildOutput) + + + + + + + $(CopyModReferencesToBuildOutput) + + + $(CopyModReferencesToBuildOutput) + + + $(CopyModReferencesToBuildOutput) + + + $(CopyModReferencesToBuildOutput) + + + $(GamePath)\Netcode.dll + $(CopyModReferencesToBuildOutput) + + + $(GamePath)\StardewValley.GameData.dll + $(CopyModReferencesToBuildOutput) + + + + + + + $(GamePath)\MonoGame.Framework.dll + $(CopyModReferencesToBuildOutput) + + + $(GamePath)\StardewValley.GameData.MonoGame.dll + $(CopyModReferencesToBuildOutput) - - - - - - false - true - - - false - true - - - false - true - - - false - true - - - $(GamePath)\Netcode.dll - False - true - - - $(GamePath)\Stardew Valley.exe - false - true - - - $(GamePath)\StardewValley.GameData.dll - false - true - - - $(GamePath)\StardewModdingAPI.exe - false - true - - - $(GamePath)\smapi-internal\SMAPI.Toolkit.CoreInterfaces.dll - false - true - - - $(GamePath)\xTile.dll - false - false - true - - - - - - Program - $(GamePath)\StardewModdingAPI.exe - $(GamePath) - - - - - - - $(GamePath)\MonoGame.Framework.dll - false - false - true - - - $(GamePath)\StardewValley.exe - false - true - - - $(GamePath)\StardewValley.GameData.MonoGame.dll - false - true - - - $(GamePath)\StardewModdingAPI.exe - false - true - - - $(GamePath)\smapi-internal\SMAPI.Toolkit.CoreInterfaces.dll - false - true - - - $(GamePath)\xTile.dll - false - true - - - - - - - + - + + Date: Wed, 19 Jun 2019 15:45:45 -0400 Subject: remove unsupported Markdown in nuspec release notes --- src/SMAPI.ModBuildConfig/package.nuspec | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec index 524cadfd..6779798d 100644 --- a/src/SMAPI.ModBuildConfig/package.nuspec +++ b/src/SMAPI.ModBuildConfig/package.nuspec @@ -15,16 +15,16 @@ 3.0.0: - Updated for SMAPI 3.0 and Stardew Valley 1.4. - - Added automatic support for `assets` folders. - - Added `$(GameExecutableName)` MSBuild variable. - - Added support for projects using the simplified `.csproj` format: + - Added automatic support for 'assets' folders. + - Added $(GameExecutableName) MSBuild variable. + - Added support for projects using the simplified .csproj format: - platform target is now set to x86 automatically to avoid mismatching platform target warnings; - added GAC to assembly search paths to fix references to XNA Framework. - Added option to disable game debugging config. - - Added `.pdb` files to builds by default (to enable line numbers in error stack traces). + - Added .pdb files to builds by default (to enable line numbers in error stack traces). - Added optional Harmony reference. - - Fixed `Newtonsoft.Json.pdb` included in release zips when Json.NET is referenced directly. - - Fixed `<IgnoreModFilePatterns>` not working for `i18n` files. + - Fixed Newtonsoft.Json.pdb included in release zips when Json.NET is referenced directly. + - Fixed <IgnoreModFilePatterns> not working for i18n files. - Dropped support for older versions of SMAPI and Visual Studio. -- cgit From 0eb986a8c0c071a617995121b66e9aee3ff910c8 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 24 Jun 2019 00:34:49 -0400 Subject: update compatibility list (#638) --- src/SMAPI.Web/wwwroot/SMAPI.metadata.json | 40 +++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Web/wwwroot/SMAPI.metadata.json b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json index f1e0023e..bde6102c 100644 --- a/src/SMAPI.Web/wwwroot/SMAPI.metadata.json +++ b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json @@ -237,11 +237,21 @@ /********* ** Broke in SDV 1.4 *********/ + "Grass Growth": { + "ID": "bcmpinc.GrassGrowth", + "~1.0 | Status": "AssumeBroken" // runtime Harmony error + }, + "Loved Labels": { "ID": "Advize.LovedLabels", "~2.2.1-unofficial.2-pathoschild | Status": "AssumeBroken" // runtime reflection errors }, + "Yet Another Harvest With Scythe Mod": { + "ID": "bcmpinc.HarvestWithScythe", + "~1.0 | Status": "AssumeBroken" // runtime Harmony error + }, + /********* ** Broke in SMAPI 3.0 (runtime errors due to lifecycle changes) *********/ @@ -271,6 +281,11 @@ "~1.6.4 | Status": "AssumeBroken" }, + "Current Location (Vrakyas)": { + "ID": "Vrakyas.CurrentLocation", + "~1.5.4 | Status": "AssumeBroken" + }, + "Custom Adventure Guild Challenges": { "ID": "DefenTheNation.CustomGuildChallenges", "~1.8 | Status": "AssumeBroken" @@ -307,6 +322,21 @@ "~1.2.7 | Status": "AssumeBroken" // possibly due to PyTK }, + "Stardew Valley ESP": { + "ID": "reimu.sdv-helper", + "~1.1 | Status": "AssumeBroken" + }, + + "Underdark Krobus": { + "ID": "melnoelle.underdarkkrobus", + "~1.0.0 | Status": "AssumeBroken" // NRE in ModEntry + }, + + "Underdark Sewer": { + "ID": "melnoelle.underdarksewer", + "~1.1.0 | Status": "AssumeBroken" // NRE in ModEntry + }, + /********* ** Broke in SDV 1.3.36 *********/ @@ -422,11 +452,6 @@ "~0.3 | Status": "AssumeBroken" // broke in 1.3: Exception from HarmonyInstance "bcmpinc.FixScytheExp" [...] Bad label content in ILGenerator. }, - "Grass Growth": { - "ID": "bcmpinc.GrassGrowth", - "~0.6 | Status": "AssumeBroken" // breaks newer versions of bcmpinc mods (per bcmpinc's request) - }, - "More Silo Storage": { "ID": "OrneryWalrus.MoreSiloStorage", "~1.0.1 | Status": "AssumeBroken" // broke in SDV 1.3 @@ -491,11 +516,6 @@ "~0.6 | Status": "AssumeBroken" // breaks newer versions of bcmpinc mods (per bcmpinc's request) }, - "Yet Another Harvest With Scythe Mod": { - "ID": "bcmpinc.HarvestWithScythe", - "~0.6 | Status": "AssumeBroken" // breaks newer versions of bcmpinc mods (per bcmpinc's request) - }, - /********* ** Broke circa SDV 1.2 *********/ -- cgit From a352b1101c4f884c2248da853d47277d534dfbe4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 6 Jul 2019 00:35:51 -0400 Subject: reorganise docs to support more detailed technical docs (#651) --- docs/mod-build-config.md | 22 +++++ docs/technical-docs.md | 236 +++++++++++------------------------------------ docs/web-services.md | 104 +++++++++++++++++++++ src/SMAPI.sln | 1 + 4 files changed, 182 insertions(+), 181 deletions(-) create mode 100644 docs/web-services.md (limited to 'src') diff --git a/docs/mod-build-config.md b/docs/mod-build-config.md index 9121c175..6c8ea2fa 100644 --- a/docs/mod-build-config.md +++ b/docs/mod-build-config.md @@ -14,6 +14,7 @@ for SMAPI mods and related tools. The package is fully compatible with Linux, Ma * [Special cases](#special-cases) * [Custom game path](#custom-game-path) * [Non-mod projects](#non-mod-projects) +* [For SMAPI developers](#for-smapi-developers) * [Release notes](#release-notes) ## Use @@ -265,6 +266,27 @@ If you need to copy the referenced DLLs into your build output, add this too: true ``` +## For SMAPI developers +The mod build package consists of three projects: + +project | purpose +------------------------------------------------- | ---------------- +`StardewModdingAPI.ModBuildConfig` | Configures the build (references, deploying the mod files, setting up debugging, etc). +`StardewModdingAPI.ModBuildConfig.Analyzer` | Adds C# analyzers which show code warnings in Visual Studio. +`StardewModdingAPI.ModBuildConfig.Analyzer.Tests` | Unit tests for the C# analyzers. + +To prepare a build of the NuGet package: +1. Install the [NuGet CLI](https://docs.microsoft.com/en-us/nuget/install-nuget-client-tools#nugetexe-cli). +1. Change the version and release notes in `package.nuspec`. +2. Rebuild the solution in _Release_ mode. +3. Open a terminal in the `bin/Pathoschild.Stardew.ModBuildConfig` package and run this command: + ```bash + nuget.exe pack + ``` + +That will create a `Pathoschild.Stardew.ModBuildConfig-.nupkg` file in the same directory +which can be uploaded to NuGet or referenced directly. + ## Release notes ### Upcoming release * Updated for SMAPI 3.0 and Stardew Valley 1.4. diff --git a/docs/technical-docs.md b/docs/technical-docs.md index 5598b7a8..5ef6dfb1 100644 --- a/docs/technical-docs.md +++ b/docs/technical-docs.md @@ -3,27 +3,60 @@ This file provides more technical documentation about SMAPI. If you only want to use or create mods, this section isn't relevant to you; see the main README to use or create mods. +This document is about SMAPI itself; see also [mod build package](mod-build-config.md) and +[web services](web-services.md). + # Contents -* [SMAPI](#smapi) - * [Development](#development) - * [Compiling from source](#compiling-from-source) - * [Debugging a local build](#debugging-a-local-build) - * [Preparing a release](#preparing-a-release) - * [Customisation](#customisation) - * [Configuration file](#configuration-file) - * [Command-line arguments](#command-line-arguments) - * [Compile flags](#compile-flags) -* [SMAPI web services](#smapi-web-services) - * [Overview](#overview) - * [Log parser](#log-parser) - * [Web API](#web-api) - * [Development](#development-2) - * [Local development](#local-development) - * [Deploying to Amazon Beanstalk](#deploying-to-amazon-beanstalk) -* [Mod build config package](#mod-build-config-package) - -# SMAPI -## Development +* [Customisation](#customisation) + * [Configuration file](#configuration-file) + * [Command-line arguments](#command-line-arguments) + * [Compile flags](#compile-flags) +* [For SMAPI developers](#for-smapi-developers) + * [Compiling from source](#compiling-from-source) + * [Debugging a local build](#debugging-a-local-build) + * [Preparing a release](#preparing-a-release) +* [Release notes](#release-notes) + +## Customisation +### Configuration file +You can customise the SMAPI behaviour by editing the `smapi-internal/config.json` file in your game +folder. + +Basic fields: + +field | purpose +----------------- | ------- +`DeveloperMode` | Default `false` (except in _SMAPI for developers_ releases). Whether to enable features intended for mod developers (mainly more detailed console logging). +`CheckForUpdates` | Default `true`. Whether SMAPI should check for a newer version when you load the game. If a new version is available, a small message will appear in the console. This doesn't affect the load time even if your connection is offline or slow, because it happens in the background. +`VerboseLogging` | Default `false`. Whether SMAPI should log more information about the game context. +`ModData` | Internal metadata about SMAPI mods. Changing this isn't recommended and may destabilise your game. See documentation in the file. + +### Command-line arguments +The SMAPI installer recognises three command-line arguments: + +argument | purpose +-------- | ------- +`--install` | Preselects the install action, skipping the prompt asking what the user wants to do. +`--uninstall` | Preselects the uninstall action, skipping the prompt asking what the user wants to do. +`--game-path "path"` | Specifies the full path to the folder containing the Stardew Valley executable, skipping automatic detection and any prompt to choose a path. If the path is not valid, the installer displays an error. + +SMAPI itself recognises two arguments, but these are intended for internal use or testing and may +change without warning. + +argument | purpose +-------- | ------- +`--no-terminal` | SMAPI won't write anything to the console window. (Messages will still be written to the log file.) +`--mods-path` | The path to search for mods, if not the standard `Mods` folder. This can be a path relative to the game folder (like `--mods-path "Mods (test)"`) or an absolute path. + +### Compile flags +SMAPI uses a small number of conditional compilation constants, which you can set by editing the +`` element in `StardewModdingAPI.csproj`. Supported constants: + +flag | purpose +---- | ------- +`SMAPI_FOR_WINDOWS` | Whether SMAPI is being compiled on Windows for players on Windows. Set automatically in `crossplatform.targets`. + +## For SMAPI developers ### Compiling from source Using an official SMAPI release is recommended for most users. @@ -68,164 +101,5 @@ on the wiki for the first-time setup. 3. Rename the folders to `SMAPI installer` and `SMAPI installer for developers`. 4. Zip the two folders. -## Customisation -### Configuration file -You can customise the SMAPI behaviour by editing the `smapi-internal/config.json` file in your game -folder. - -Basic fields: - -field | purpose ------------------ | ------- -`DeveloperMode` | Default `false` (except in _SMAPI for developers_ releases). Whether to enable features intended for mod developers (mainly more detailed console logging). -`CheckForUpdates` | Default `true`. Whether SMAPI should check for a newer version when you load the game. If a new version is available, a small message will appear in the console. This doesn't affect the load time even if your connection is offline or slow, because it happens in the background. -`VerboseLogging` | Default `false`. Whether SMAPI should log more information about the game context. -`ModData` | Internal metadata about SMAPI mods. Changing this isn't recommended and may destabilise your game. See documentation in the file. - -### Command-line arguments -The SMAPI installer recognises three command-line arguments: - -argument | purpose --------- | ------- -`--install` | Preselects the install action, skipping the prompt asking what the user wants to do. -`--uninstall` | Preselects the uninstall action, skipping the prompt asking what the user wants to do. -`--game-path "path"` | Specifies the full path to the folder containing the Stardew Valley executable, skipping automatic detection and any prompt to choose a path. If the path is not valid, the installer displays an error. - -SMAPI itself recognises two arguments, but these are intended for internal use or testing and may -change without warning. - -argument | purpose --------- | ------- -`--no-terminal` | SMAPI won't write anything to the console window. (Messages will still be written to the log file.) -`--mods-path` | The path to search for mods, if not the standard `Mods` folder. This can be a path relative to the game folder (like `--mods-path "Mods (test)"`) or an absolute path. - -### Compile flags -SMAPI uses a small number of conditional compilation constants, which you can set by editing the -`` element in `StardewModdingAPI.csproj`. Supported constants: - -flag | purpose ----- | ------- -`SMAPI_FOR_WINDOWS` | Whether SMAPI is being compiled on Windows for players on Windows. Set automatically in `crossplatform.targets`. - -# SMAPI web services -## Overview -The `StardewModdingAPI.Web` project provides an API and web UI hosted at `*.smapi.io`. - -### Log parser -The log parser provides a web UI for uploading, parsing, and sharing SMAPI logs. The logs are -persisted in a compressed form to Pastebin. - -The log parser lives at https://log.smapi.io. - -### Web API -SMAPI provides a web API at `api.smapi.io` for use by SMAPI and external tools. The URL includes a -`{version}` token, which is the SMAPI version for backwards compatibility. This API is publicly -accessible but not officially released; it may change at any time. - -The API has one `/mods` endpoint. This provides mod info, including official versions and URLs -(from Chucklefish, GitHub, or Nexus), unofficial versions from the wiki, and optional mod metadata -from the wiki and SMAPI's internal data. This is used by SMAPI to perform update checks, and by -external tools to fetch mod data. - -The API accepts a `POST` request with the mods to match, each of which **must** specify an ID and -may _optionally_ specify [update keys](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest#Update_checks). -The API will automatically try to fetch known update keys from the wiki and internal data based on -the given ID. - -``` -POST https://api.smapi.io/v2.0/mods -{ - "mods": [ - { - "id": "Pathoschild.LookupAnything", - "updateKeys": [ "nexus:541", "chucklefish:4250" ] - } - ], - "includeExtendedMetadata": true -} -``` - -The API will automatically aggregate versions and errors. Each mod will include... -* an `id` (matching what you passed in); -* up to three versions: `main` (e.g. 'latest version' field on Nexus), `optional` if newer (e.g. - optional files on Nexus), and `unofficial` if newer (from the wiki); -* `metadata` with mod info crossreferenced from the wiki and internal data (only if you specified - `includeExtendedMetadata: true`); -* and `errors` containing any error messages that occurred while fetching data. - -For example: -``` -[ - { - "id": "Pathoschild.LookupAnything", - "main": { - "version": "1.19", - "url": "https://www.nexusmods.com/stardewvalley/mods/541" - }, - "metadata": { - "id": [ - "Pathoschild.LookupAnything", - "LookupAnything" - ], - "name": "Lookup Anything", - "nexusID": 541, - "gitHubRepo": "Pathoschild/StardewMods", - "compatibilityStatus": "Ok", - "compatibilitySummary": "✓ use latest version." - }, - "errors": [] - } -] -``` - -## Development -### Local development -`StardewModdingAPI.Web` is a regular ASP.NET MVC Core app, so you can just launch it from within -Visual Studio to run a local version. - -There are two differences when it's run locally: all endpoints use HTTP instead of HTTPS, and the -subdomain portion becomes a route (e.g. `log.smapi.io` → `localhost:59482/log`). - -Before running it locally, you need to enter your credentials in the `appsettings.Development.json` -file. See the next section for a description of each setting. This file is listed in `.gitignore` -to prevent accidentally committing credentials. - -### Deploying to Amazon Beanstalk -The app can be deployed to a standard Amazon Beanstalk IIS environment. When creating the -environment, make sure to specify the following environment properties: - -property name | description -------------------------------- | ----------------- -`LogParser:PastebinDevKey` | The [Pastebin developer key](https://pastebin.com/api#1) used to authenticate with the Pastebin API. -`LogParser:PastebinUserKey` | The [Pastebin user key](https://pastebin.com/api#8) used to authenticate with the Pastebin API. Can be left blank to post anonymously. -`LogParser:SectionUrl` | The root URL of the log page, like `https://log.smapi.io/`. -`ModUpdateCheck:GitHubPassword` | The password with which to authenticate to GitHub when fetching release info. -`ModUpdateCheck:GitHubUsername` | The username with which to authenticate to GitHub when fetching release info. - -## Mod build config package -### Overview -The mod build config package is a NuGet package that mods reference to automatically set up -references, configure the build, and add analyzers specific to Stardew Valley mods. - -This involves three projects: - -project | purpose -------------------------------------------------- | ---------------- -`StardewModdingAPI.ModBuildConfig` | Configures the build (references, deploying the mod files, setting up debugging, etc). -`StardewModdingAPI.ModBuildConfig.Analyzer` | Adds C# analyzers which show code warnings in Visual Studio. -`StardewModdingAPI.ModBuildConfig.Analyzer.Tests` | Unit tests for the C# analyzers. - -When the projects are built, the relevant files are copied into `bin/Pathoschild.Stardew.ModBuildConfig`. - -### Preparing a build -To prepare a build of the NuGet package: -1. Install the [NuGet CLI](https://docs.microsoft.com/en-us/nuget/install-nuget-client-tools#nugetexe-cli). -1. Change the version and release notes in `package.nuspec`. -2. Rebuild the solution in _Release_ mode. -3. Open a terminal in the `bin/Pathoschild.Stardew.ModBuildConfig` package and run this command: - ```bash - nuget.exe pack - ``` - -That will create a `Pathoschild.Stardew.ModBuildConfig-.nupkg` file in the same directory -which can be uploaded to NuGet or referenced directly. +## Release notes +See [release notes](release-notes.md). diff --git a/docs/web-services.md b/docs/web-services.md new file mode 100644 index 00000000..601152de --- /dev/null +++ b/docs/web-services.md @@ -0,0 +1,104 @@ +**SMAPI.Web** contains the code for the `smapi.io` website, including the mod compatibility list +and update check API. + +## Contents +* [Overview](#overview) + * [Log parser](#log-parser) + * [Web API](#web-api) +* [For SMAPI developers](#for-smapi-developers) + * [Local development](#local-development) + * [Deploying to Amazon Beanstalk](#deploying-to-amazon-beanstalk) + +## Overview +The `StardewModdingAPI.Web` project provides an API and web UI hosted at `*.smapi.io`. + +### Log parser +The log parser provides a web UI for uploading, parsing, and sharing SMAPI logs. The logs are +persisted in a compressed form to Pastebin. + +The log parser lives at https://log.smapi.io. + +### Web API +SMAPI provides a web API at `api.smapi.io` for use by SMAPI and external tools. The URL includes a +`{version}` token, which is the SMAPI version for backwards compatibility. This API is publicly +accessible but not officially released; it may change at any time. + +The API has one `/mods` endpoint. This provides mod info, including official versions and URLs +(from Chucklefish, GitHub, or Nexus), unofficial versions from the wiki, and optional mod metadata +from the wiki and SMAPI's internal data. This is used by SMAPI to perform update checks, and by +external tools to fetch mod data. + +The API accepts a `POST` request with the mods to match, each of which **must** specify an ID and +may _optionally_ specify [update keys](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest#Update_checks). +The API will automatically try to fetch known update keys from the wiki and internal data based on +the given ID. + +``` +POST https://api.smapi.io/v2.0/mods +{ + "mods": [ + { + "id": "Pathoschild.LookupAnything", + "updateKeys": [ "nexus:541", "chucklefish:4250" ] + } + ], + "includeExtendedMetadata": true +} +``` + +The API will automatically aggregate versions and errors. Each mod will include... +* an `id` (matching what you passed in); +* up to three versions: `main` (e.g. 'latest version' field on Nexus), `optional` if newer (e.g. + optional files on Nexus), and `unofficial` if newer (from the wiki); +* `metadata` with mod info crossreferenced from the wiki and internal data (only if you specified + `includeExtendedMetadata: true`); +* and `errors` containing any error messages that occurred while fetching data. + +For example: +``` +[ + { + "id": "Pathoschild.LookupAnything", + "main": { + "version": "1.19", + "url": "https://www.nexusmods.com/stardewvalley/mods/541" + }, + "metadata": { + "id": [ + "Pathoschild.LookupAnything", + "LookupAnything" + ], + "name": "Lookup Anything", + "nexusID": 541, + "gitHubRepo": "Pathoschild/StardewMods", + "compatibilityStatus": "Ok", + "compatibilitySummary": "✓ use latest version." + }, + "errors": [] + } +] +``` + +## For SMAPI developers +### Local development +`StardewModdingAPI.Web` is a regular ASP.NET MVC Core app, so you can just launch it from within +Visual Studio to run a local version. + +There are two differences when it's run locally: all endpoints use HTTP instead of HTTPS, and the +subdomain portion becomes a route (e.g. `log.smapi.io` → `localhost:59482/log`). + +Before running it locally, you need to enter your credentials in the `appsettings.Development.json` +file. See the next section for a description of each setting. This file is listed in `.gitignore` +to prevent accidentally committing credentials. + +### Deploying to Amazon Beanstalk +The app can be deployed to a standard Amazon Beanstalk IIS environment. When creating the +environment, make sure to specify the following environment properties: + +property name | description +------------------------------- | ----------------- +`LogParser:PastebinDevKey` | The [Pastebin developer key](https://pastebin.com/api#1) used to authenticate with the Pastebin API. +`LogParser:PastebinUserKey` | The [Pastebin user key](https://pastebin.com/api#8) used to authenticate with the Pastebin API. Can be left blank to post anonymously. +`LogParser:SectionUrl` | The root URL of the log page, like `https://log.smapi.io/`. +`ModUpdateCheck:GitHubPassword` | The password with which to authenticate to GitHub when fetching release info. +`ModUpdateCheck:GitHubUsername` | The username with which to authenticate to GitHub when fetching release info. diff --git a/src/SMAPI.sln b/src/SMAPI.sln index 3a8ed920..08a47a46 100644 --- a/src/SMAPI.sln +++ b/src/SMAPI.sln @@ -39,6 +39,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{EB35A917-6 ..\docs\README.md = ..\docs\README.md ..\docs\release-notes.md = ..\docs\release-notes.md ..\docs\technical-docs.md = ..\docs\technical-docs.md + ..\docs\web-services.md = ..\docs\web-services.md EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Internal", "Internal", "{82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11}" -- cgit From 1dde811c36c2d03c17df4d09d978907bbd608787 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 6 Jul 2019 01:04:05 -0400 Subject: group technical docs, add general shortcut for mod build package docs (#651) --- build/common.targets | 2 +- docs/README.md | 8 +- docs/mod-build-config.md | 369 -------------------- docs/release-notes.md | 4 +- docs/screenshots/code-analyzer-example.png | Bin 3473 -> 0 bytes docs/technical-docs.md | 105 ------ docs/technical/mod-package.md | 371 +++++++++++++++++++++ .../screenshots/code-analyzer-example.png | Bin 0 -> 3473 bytes docs/technical/smapi.md | 105 ++++++ docs/technical/web.md | 106 ++++++ docs/web-services.md | 104 ------ .../NetFieldAnalyzerTests.cs | 4 +- .../ObsoleteFieldAnalyzerTests.cs | 2 +- .../NetFieldAnalyzer.cs | 8 +- .../ObsoleteFieldAnalyzer.cs | 4 +- .../SMAPI.ModBuildConfig.csproj | 4 +- src/SMAPI.ModBuildConfig/build/smapi.targets | 2 +- src/SMAPI.ModBuildConfig/package.nuspec | 4 +- src/SMAPI.Web/Startup.cs | 2 +- src/SMAPI.sln | 11 +- 20 files changed, 612 insertions(+), 603 deletions(-) delete mode 100644 docs/mod-build-config.md delete mode 100644 docs/screenshots/code-analyzer-example.png delete mode 100644 docs/technical-docs.md create mode 100644 docs/technical/mod-package.md create mode 100644 docs/technical/screenshots/code-analyzer-example.png create mode 100644 docs/technical/smapi.md create mode 100644 docs/technical/web.md delete mode 100644 docs/web-services.md (limited to 'src') diff --git a/build/common.targets b/build/common.targets index 804f1ed3..59d5bfd2 100644 --- a/build/common.targets +++ b/build/common.targets @@ -10,7 +10,7 @@ - + diff --git a/docs/README.md b/docs/README.md index e4220de2..4b9c97a1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -38,16 +38,16 @@ doesn't change any of your game files. It serves eight main purposes: something goes wrong. (Via the bundled SaveBackup mod.)_ ## Documentation -Have questions? Come [chat on Discord](https://discord.gg/KCJHWhX) with SMAPI developers and other -modders! +Have questions? Come [chat on Discord](https://stardewvalleywiki.com/Modding:Community) with SMAPI +developers and other modders! ### For players * [Player guide](https://stardewvalleywiki.com/Modding:Player_Guide) ### For modders * [Modding documentation](https://stardewvalleywiki.com/Modding:Index) -* [Mod build configuration](mod-build-config.md) +* [Mod build configuration](technical/mod-package.md) * [Release notes](release-notes.md) ### For SMAPI developers -* [Technical docs](technical-docs.md) +* [Technical docs](technical/smapi.md) diff --git a/docs/mod-build-config.md b/docs/mod-build-config.md deleted file mode 100644 index 6c8ea2fa..00000000 --- a/docs/mod-build-config.md +++ /dev/null @@ -1,369 +0,0 @@ -The **mod build package** is an open-source NuGet package which automates the MSBuild configuration -for SMAPI mods and related tools. The package is fully compatible with Linux, Mac, and Windows. - -## Contents -* [Use](#use) -* [Features](#features) - * [Detect game path](#detect-game-path) - * [Add assembly references](#add-assembly-references) - * [Copy files into the `Mods` folder and create release zip](#copy-files-into-the-mods-folder-and-create-release-zip) - * [Launch or debug game](#launch-or-debug-game) - * [Preconfigure common settings](#preconfigure-common-settings) - * [Add code warnings](#add-code-warnings) -* [Code warnings](#code-warnings) -* [Special cases](#special-cases) - * [Custom game path](#custom-game-path) - * [Non-mod projects](#non-mod-projects) -* [For SMAPI developers](#for-smapi-developers) -* [Release notes](#release-notes) - -## Use -1. Create an empty library project. -2. Reference the [`Pathoschild.Stardew.ModBuildConfig` NuGet package](https://www.nuget.org/packages/Pathoschild.Stardew.ModBuildConfig). -3. [Write your code](https://stardewvalleywiki.com/Modding:Creating_a_SMAPI_mod). -4. Compile on any platform. -5. Run the game to play with your mod. - -## Features -The package automatically makes the changes listed below. In some cases you can configure how it -works by editing your mod's `.csproj` file, and adding the given properties between the first -`` and `` tags. - -### Detect game path -The package finds your game folder by scanning the default install paths and Windows registry. It -adds two MSBuild properties for use in your `.csproj` file if needed: - -property | description --------- | ----------- -`$(GamePath)` | The absolute path to the detected game folder. -`$(GameExecutableName)` | The game's executable name for the current OS (`Stardew Valley` on Windows, or `StardewValley` on Linux/Mac). - -If you get a build error saying it can't find your game, see [_set the game path_](#set-the-game-path). - -### Add assembly references -The package adds assembly references to SMAPI, Stardew Valley, xTile, and MonoGame (Linux/Mac) or XNA -Framework (Windows). It automatically adjusts depending on which OS you're compiling it on. - -The assemblies aren't copied to the build output, since mods loaded by SMAPI won't need them. For -non-mod projects like unit tests, you can set this property: -```xml -true -``` - -If your mod uses [Harmony](https://github.com/pardeike/Harmony) (not recommended for most mods), -the package can add a reference to SMAPI's Harmony DLL for you: -```xml -true -``` - -### Copy files into the `Mods` folder and create release zip -
-
Files considered part of your mod
-
- -These files are selected by default: `manifest.json`, -[`i18n` files](https://stardewvalleywiki.com/Modding:Translations) (if any), the `assets` folder -(if any), and all files in the build output. You can select custom files by [adding them to the -build output](https://stackoverflow.com/a/10828462/262123). (If your project references another mod, -make sure the reference is [_not_ marked 'copy local'](https://msdn.microsoft.com/en-us/library/t1zz5y8c(v=vs.100).aspx).) - -You can deselect a file by removing it from the build output. For a default file, you can set the -property below to a comma-delimited list of regex patterns to ignore. For crossplatform -compatibility, you should replace path delimiters with `[/\\]`. - -```xml -\.txt$, \.pdf$, assets[/\\]paths.png -``` - -
-
Copy files into the `Mods` folder
-
- -The package copies the selected files into your game's `Mods` folder when you rebuild the code, -with a subfolder matching the mod's project name. - -You can change the folder name: -```xml -YourModName -``` - -Or disable deploying the files: -```xml -false -``` - -
-
Create release zip
-
- -The package adds a zip file in your project's `bin` folder when you rebuild the code, in the format -recommended for sites like Nexus Mods. The zip filename can be changed using `ModFolderName` above. - -You can change the folder path where the zip is created: -```xml -$(SolutionDir)\_releases -``` - -Or disable zip creation: -```xml -false -``` - -
-
- -### Launch or debug game -On Windows only, the package configures Visual Studio so you can launch the game and attach a -debugger using _Debug > Start Debugging_ or _Debug > Start Without Debugging_. This lets you [set -breakpoints](https://docs.microsoft.com/en-us/visualstudio/debugger/using-breakpoints?view=vs-2019) -in your code while the game is running, or [make simple changes to the mod code without needing to -restart the game](https://docs.microsoft.com/en-us/visualstudio/debugger/edit-and-continue?view=vs-2019). - -This is disabled on Linux/Mac due to limitations with the Mono wrapper. - -To disable game debugging (only needed for some non-mod projects): - -```xml -false -``` - -### Preconfigure common settings -The package automatically enables PDB files, so error logs show line numbers for simpler debugging. - -For projects using the simplified `.csproj` format, it also enables the GAC (to support XNA -Framework) and sets the build to x86 mode (to avoid 'mismatch between the processor architecture' warnings due to - the game being x86). - -### Add code warnings -The package runs code analysis on your mod and raises warnings for some common errors or pitfalls. -See [_code warnings_](#code-warnings) for more info. - -## Code warnings -### Overview -The NuGet package adds code warnings in Visual Studio specific to Stardew Valley. For example: -![](screenshots/code-analyzer-example.png) - -You can hide the warnings using the warning ID (shown under 'code' in the Error List). See... -* [for specific code](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/preprocessor-directives/preprocessor-pragma-warning); -* for a method using this attribute: - ```cs - [System.Diagnostics.CodeAnalysis.SuppressMessage("SMAPI.CommonErrors", "AvoidNetField")] - ``` -* for an entire project: - 1. Expand the _References_ node for the project in Visual Studio. - 2. Right-click on _Analyzers_ and choose _Open Active Rule Set_. - 4. Expand _StardewModdingAPI.ModBuildConfig.Analyzer_ and uncheck the warnings you want to hide. - -See below for help with each specific warning. - -### Avoid implicit net field cast -Warning text: -> This implicitly converts '{{expression}}' from {{net type}} to {{other type}}, but -> {{net type}} has unintuitive implicit conversion rules. Consider comparing against the actual -> value instead to avoid bugs. - -Stardew Valley uses net types (like `NetBool` and `NetInt`) to handle multiplayer sync. These types -can implicitly convert to their equivalent normal values (like `bool x = new NetBool()`), but their -conversion rules are unintuitive and error-prone. For example, -`item?.category == null && item?.category != null` can both be true at once, and -`building.indoors != null` can be true for a null value. - -Suggested fix: -* Some net fields have an equivalent non-net property like `monster.Health` (`int`) instead of - `monster.health` (`NetInt`). The package will add a separate [AvoidNetField](#avoid-net-field) warning for - these. Use the suggested property instead. -* For a reference type (i.e. one that can contain `null`), you can use the `.Value` property: - ```c# - if (building.indoors.Value == null) - ``` - Or convert the value before comparison: - ```c# - GameLocation indoors = building.indoors; - if(indoors == null) - // ... - ``` -* For a value type (i.e. one that can't contain `null`), check if the object is null (if applicable) - and compare with `.Value`: - ```cs - if (item != null && item.category.Value == 0) - ``` - -### Avoid net field -Warning text: -> '{{expression}}' is a {{net type}} field; consider using the {{property name}} property instead. - -Your code accesses a net field, which has some unusual behavior (see [AvoidImplicitNetFieldCast](#avoid-implicit-net-field-cast)). -This field has an equivalent non-net property that avoids those issues. - -Suggested fix: access the suggested property name instead. - -### Avoid obsolete field -Warning text: -> The '{{old field}}' field is obsolete and should be replaced with '{{new field}}'. - -Your code accesses a field which is obsolete or no longer works. Use the suggested field instead. - -## Special cases -### Custom game path -The package usually detects where your game is installed automatically. If it can't find your game -or you have multiple installs, you can specify the path yourself. There's two ways to do that: - -* **Option 1: global game path (recommended).** - _This will apply to every project that uses the package._ - - 1. Get the full folder path containing the Stardew Valley executable. - 2. Create this file: - - platform | path - --------- | ---- - Linux/Mac | `~/stardewvalley.targets` - Windows | `%USERPROFILE%\stardewvalley.targets` - - 3. Save the file with this content: - - ```xml - - - PATH_HERE - - - ``` - - 4. Replace `PATH_HERE` with your game path. - -* **Option 2: path in the project file.** - _You'll need to do this for each project that uses the package._ - - 1. Get the folder path containing the Stardew Valley `.exe` file. - 2. Add this to your `.csproj` file under the ` - PATH_HERE - - ``` - - 3. Replace `PATH_HERE` with your custom game install path. - -The configuration will check your custom path first, then fall back to the default paths (so it'll -still compile on a different computer). - -You access the game path via `$(GamePath)` in MSBuild properties, if you need to reference another -file in the game folder. - -### Non-mod projects -You can use the package in non-mod projects too (e.g. unit tests or framework DLLs). Just disable -the mod-related package features: - -```xml -false -false -false -``` - -If you need to copy the referenced DLLs into your build output, add this too: -```xml -true -``` - -## For SMAPI developers -The mod build package consists of three projects: - -project | purpose -------------------------------------------------- | ---------------- -`StardewModdingAPI.ModBuildConfig` | Configures the build (references, deploying the mod files, setting up debugging, etc). -`StardewModdingAPI.ModBuildConfig.Analyzer` | Adds C# analyzers which show code warnings in Visual Studio. -`StardewModdingAPI.ModBuildConfig.Analyzer.Tests` | Unit tests for the C# analyzers. - -To prepare a build of the NuGet package: -1. Install the [NuGet CLI](https://docs.microsoft.com/en-us/nuget/install-nuget-client-tools#nugetexe-cli). -1. Change the version and release notes in `package.nuspec`. -2. Rebuild the solution in _Release_ mode. -3. Open a terminal in the `bin/Pathoschild.Stardew.ModBuildConfig` package and run this command: - ```bash - nuget.exe pack - ``` - -That will create a `Pathoschild.Stardew.ModBuildConfig-.nupkg` file in the same directory -which can be uploaded to NuGet or referenced directly. - -## Release notes -### Upcoming release -* Updated for SMAPI 3.0 and Stardew Valley 1.4. -* Added automatic support for `assets` folders. -* Added `$(GameExecutableName)` MSBuild variable. -* Added support for projects using the simplified `.csproj` format: - * platform target is now set to x86 automatically to avoid mismatching platform target warnings; - * added GAC to assembly search paths to fix references to XNA Framework. -* Added option to disable game debugging config. -* Added `.pdb` files to builds by default (to enable line numbers in error stack traces). -* Added optional Harmony reference. -* Fixed `Newtonsoft.Json.pdb` included in release zips when Json.NET is referenced directly. -* Fixed `` not working for `i18n` files. -* Dropped support for older versions of SMAPI and Visual Studio. - -### 2.2 -* Added support for SMAPI 2.8+ (still compatible with earlier versions). -* Added default game paths for 32-bit Windows. -* Fixed valid manifests marked invalid in some cases. - -### 2.1 -* Added support for Stardew Valley 1.3. -* Added support for non-mod projects. -* Added C# analyzers to warn about implicit conversions of Netcode fields in Stardew Valley 1.3. -* Added option to ignore files by regex pattern. -* Added reference to new SMAPI DLL. -* Fixed some game paths not detected by NuGet package. - -### 2.0.2 -* Fixed compatibility issue on Linux. - -### 2.0.1 -* Fixed mod deploy failing to create subfolders if they don't already exist. - -### 2.0 -* Added: mods are now copied into the `Mods` folder automatically (configurable). -* Added: release zips are now created automatically in your build output folder (configurable). -* Added: mod deploy and release zips now exclude Json.NET automatically, since it's provided by SMAPI. -* Added mod's version to release zip filename. -* Improved errors to simplify troubleshooting. -* Fixed release zip not having a mod folder. -* Fixed release zip failing if mod name contains characters that aren't valid in a filename. - -### 1.7.1 -* Fixed issue where i18n folders were flattened. -* The manifest/i18n files in the project now take precedence over those in the build output if both - are present. - -### 1.7 -* Added option to create release zips on build. -* Added reference to XNA's XACT library for audio-related mods. - -### 1.6 -* Added support for deploying mod files into `Mods` automatically. -* Added a build error if a game folder is found, but doesn't contain Stardew Valley or SMAPI. - -### 1.5 -* Added support for setting a custom game path globally. -* Added default GOG path on Mac. - -### 1.4 -* Fixed detection of non-default game paths on 32-bit Windows. -* Removed support for SilVerPLuM (discontinued). -* Removed support for overriding the target platform (no longer needed since SMAPI crossplatforms - mods automatically). - -### 1.3 -* Added support for non-default game paths on Windows. - -### 1.2 -* Exclude game binaries from mod build output. - -### 1.1 -* Added support for overriding the target platform. - -### 1.0 -* Initial release. -* Added support for detecting the game path automatically. -* Added support for injecting XNA/MonoGame references automatically based on the OS. -* Added support for mod builders like SilVerPLuM. diff --git a/docs/release-notes.md b/docs/release-notes.md index efac8f41..3f955e78 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,3 +1,5 @@ +← [README](README.md) + # Release notes ## 3.0 (upcoming release) These changes have not been released yet. @@ -674,7 +676,7 @@ Released 14 October 2017 for Stardew Valley 1.2.30–1.2.33. * **Command-line install** For power users and mod managers, the SMAPI installer can now be scripted using command-line arguments - (see [technical docs](technical-docs.md#command-line-arguments)). + (see [technical docs](technical/smapi.md#command-line-arguments)). ### Change log For players: diff --git a/docs/screenshots/code-analyzer-example.png b/docs/screenshots/code-analyzer-example.png deleted file mode 100644 index de38f643..00000000 Binary files a/docs/screenshots/code-analyzer-example.png and /dev/null differ diff --git a/docs/technical-docs.md b/docs/technical-docs.md deleted file mode 100644 index 5ef6dfb1..00000000 --- a/docs/technical-docs.md +++ /dev/null @@ -1,105 +0,0 @@ -← [README](README.md) - -This file provides more technical documentation about SMAPI. If you only want to use or create -mods, this section isn't relevant to you; see the main README to use or create mods. - -This document is about SMAPI itself; see also [mod build package](mod-build-config.md) and -[web services](web-services.md). - -# Contents -* [Customisation](#customisation) - * [Configuration file](#configuration-file) - * [Command-line arguments](#command-line-arguments) - * [Compile flags](#compile-flags) -* [For SMAPI developers](#for-smapi-developers) - * [Compiling from source](#compiling-from-source) - * [Debugging a local build](#debugging-a-local-build) - * [Preparing a release](#preparing-a-release) -* [Release notes](#release-notes) - -## Customisation -### Configuration file -You can customise the SMAPI behaviour by editing the `smapi-internal/config.json` file in your game -folder. - -Basic fields: - -field | purpose ------------------ | ------- -`DeveloperMode` | Default `false` (except in _SMAPI for developers_ releases). Whether to enable features intended for mod developers (mainly more detailed console logging). -`CheckForUpdates` | Default `true`. Whether SMAPI should check for a newer version when you load the game. If a new version is available, a small message will appear in the console. This doesn't affect the load time even if your connection is offline or slow, because it happens in the background. -`VerboseLogging` | Default `false`. Whether SMAPI should log more information about the game context. -`ModData` | Internal metadata about SMAPI mods. Changing this isn't recommended and may destabilise your game. See documentation in the file. - -### Command-line arguments -The SMAPI installer recognises three command-line arguments: - -argument | purpose --------- | ------- -`--install` | Preselects the install action, skipping the prompt asking what the user wants to do. -`--uninstall` | Preselects the uninstall action, skipping the prompt asking what the user wants to do. -`--game-path "path"` | Specifies the full path to the folder containing the Stardew Valley executable, skipping automatic detection and any prompt to choose a path. If the path is not valid, the installer displays an error. - -SMAPI itself recognises two arguments, but these are intended for internal use or testing and may -change without warning. - -argument | purpose --------- | ------- -`--no-terminal` | SMAPI won't write anything to the console window. (Messages will still be written to the log file.) -`--mods-path` | The path to search for mods, if not the standard `Mods` folder. This can be a path relative to the game folder (like `--mods-path "Mods (test)"`) or an absolute path. - -### Compile flags -SMAPI uses a small number of conditional compilation constants, which you can set by editing the -`` element in `StardewModdingAPI.csproj`. Supported constants: - -flag | purpose ----- | ------- -`SMAPI_FOR_WINDOWS` | Whether SMAPI is being compiled on Windows for players on Windows. Set automatically in `crossplatform.targets`. - -## For SMAPI developers -### Compiling from source -Using an official SMAPI release is recommended for most users. - -SMAPI uses some C# 7 code, so you'll need at least -[Visual Studio 2017](https://www.visualstudio.com/vs/community/) on Windows, -[MonoDevelop 7.0](https://www.monodevelop.com/) on Linux, -[Visual Studio 2017 for Mac](https://www.visualstudio.com/vs/visual-studio-mac/), or an equivalent -IDE to compile it. It uses build configuration derived from the -[crossplatform mod config](https://github.com/Pathoschild/Stardew.ModBuildConfig#readme) to detect -your current OS automatically and load the correct references. Compile output will be placed in a -`bin` folder at the root of the git repository. - -### Debugging a local build -Rebuilding the solution in debug mode will copy the SMAPI files into your game folder. Starting -the `StardewModdingAPI` project with debugging from Visual Studio (on Mac or Windows) will launch -SMAPI with the debugger attached, so you can intercept errors and step through the code being -executed. This doesn't work in MonoDevelop on Linux, unfortunately. - -### Preparing a release -To prepare a crossplatform SMAPI release, you'll need to compile it on two platforms. See -[crossplatforming info](https://stardewvalleywiki.com/Modding:Modder_Guide/Test_and_Troubleshoot#Testing_on_all_platforms) -on the wiki for the first-time setup. - -1. Update the version number in `GlobalAssemblyInfo.cs` and `Constants::Version`. Make sure you use a - [semantic version](https://semver.org). Recommended format: - - build type | format | example - :--------- | :----------------------- | :------ - dev build | `-alpha.` | `3.0-alpha.20171230` - prerelease | `-beta.` | `3.0-beta.2` - release | `` | `3.0` - -2. In Windows: - 1. Rebuild the solution in Release mode. - 2. Copy `windows-install.*` from `bin/SMAPI installer` and `bin/SMAPI installer for developers` to - Linux/Mac. - -3. In Linux/Mac: - 1. Rebuild the solution in Release mode. - 2. Add the `windows-install.*` files to the `bin/SMAPI installer` and - `bin/SMAPI installer for developers` folders. - 3. Rename the folders to `SMAPI installer` and `SMAPI installer for developers`. - 4. Zip the two folders. - -## Release notes -See [release notes](release-notes.md). diff --git a/docs/technical/mod-package.md b/docs/technical/mod-package.md new file mode 100644 index 00000000..43682bfb --- /dev/null +++ b/docs/technical/mod-package.md @@ -0,0 +1,371 @@ +← [SMAPI](../README.md) + +The **mod build package** is an open-source NuGet package which automates the MSBuild configuration +for SMAPI mods and related tools. The package is fully compatible with Linux, Mac, and Windows. + +## Contents +* [Use](#use) +* [Features](#features) + * [Detect game path](#detect-game-path) + * [Add assembly references](#add-assembly-references) + * [Copy files into the `Mods` folder and create release zip](#copy-files-into-the-mods-folder-and-create-release-zip) + * [Launch or debug game](#launch-or-debug-game) + * [Preconfigure common settings](#preconfigure-common-settings) + * [Add code warnings](#add-code-warnings) +* [Code warnings](#code-warnings) +* [Special cases](#special-cases) + * [Custom game path](#custom-game-path) + * [Non-mod projects](#non-mod-projects) +* [For SMAPI developers](#for-smapi-developers) +* [Release notes](#release-notes) + +## Use +1. Create an empty library project. +2. Reference the [`Pathoschild.Stardew.ModBuildConfig` NuGet package](https://www.nuget.org/packages/Pathoschild.Stardew.ModBuildConfig). +3. [Write your code](https://stardewvalleywiki.com/Modding:Creating_a_SMAPI_mod). +4. Compile on any platform. +5. Run the game to play with your mod. + +## Features +The package automatically makes the changes listed below. In some cases you can configure how it +works by editing your mod's `.csproj` file, and adding the given properties between the first +`` and `` tags. + +### Detect game path +The package finds your game folder by scanning the default install paths and Windows registry. It +adds two MSBuild properties for use in your `.csproj` file if needed: + +property | description +-------- | ----------- +`$(GamePath)` | The absolute path to the detected game folder. +`$(GameExecutableName)` | The game's executable name for the current OS (`Stardew Valley` on Windows, or `StardewValley` on Linux/Mac). + +If you get a build error saying it can't find your game, see [_set the game path_](#set-the-game-path). + +### Add assembly references +The package adds assembly references to SMAPI, Stardew Valley, xTile, and MonoGame (Linux/Mac) or XNA +Framework (Windows). It automatically adjusts depending on which OS you're compiling it on. + +The assemblies aren't copied to the build output, since mods loaded by SMAPI won't need them. For +non-mod projects like unit tests, you can set this property: +```xml +true +``` + +If your mod uses [Harmony](https://github.com/pardeike/Harmony) (not recommended for most mods), +the package can add a reference to SMAPI's Harmony DLL for you: +```xml +true +``` + +### Copy files into the `Mods` folder and create release zip +
+
Files considered part of your mod
+
+ +These files are selected by default: `manifest.json`, +[`i18n` files](https://stardewvalleywiki.com/Modding:Translations) (if any), the `assets` folder +(if any), and all files in the build output. You can select custom files by [adding them to the +build output](https://stackoverflow.com/a/10828462/262123). (If your project references another mod, +make sure the reference is [_not_ marked 'copy local'](https://msdn.microsoft.com/en-us/library/t1zz5y8c(v=vs.100).aspx).) + +You can deselect a file by removing it from the build output. For a default file, you can set the +property below to a comma-delimited list of regex patterns to ignore. For crossplatform +compatibility, you should replace path delimiters with `[/\\]`. + +```xml +\.txt$, \.pdf$, assets[/\\]paths.png +``` + +
+
Copy files into the `Mods` folder
+
+ +The package copies the selected files into your game's `Mods` folder when you rebuild the code, +with a subfolder matching the mod's project name. + +You can change the folder name: +```xml +YourModName +``` + +Or disable deploying the files: +```xml +false +``` + +
+
Create release zip
+
+ +The package adds a zip file in your project's `bin` folder when you rebuild the code, in the format +recommended for sites like Nexus Mods. The zip filename can be changed using `ModFolderName` above. + +You can change the folder path where the zip is created: +```xml +$(SolutionDir)\_releases +``` + +Or disable zip creation: +```xml +false +``` + +
+
+ +### Launch or debug game +On Windows only, the package configures Visual Studio so you can launch the game and attach a +debugger using _Debug > Start Debugging_ or _Debug > Start Without Debugging_. This lets you [set +breakpoints](https://docs.microsoft.com/en-us/visualstudio/debugger/using-breakpoints?view=vs-2019) +in your code while the game is running, or [make simple changes to the mod code without needing to +restart the game](https://docs.microsoft.com/en-us/visualstudio/debugger/edit-and-continue?view=vs-2019). + +This is disabled on Linux/Mac due to limitations with the Mono wrapper. + +To disable game debugging (only needed for some non-mod projects): + +```xml +false +``` + +### Preconfigure common settings +The package automatically enables PDB files, so error logs show line numbers for simpler debugging. + +For projects using the simplified `.csproj` format, it also enables the GAC (to support XNA +Framework) and sets the build to x86 mode (to avoid 'mismatch between the processor architecture' warnings due to + the game being x86). + +### Add code warnings +The package runs code analysis on your mod and raises warnings for some common errors or pitfalls. +See [_code warnings_](#code-warnings) for more info. + +## Code warnings +### Overview +The NuGet package adds code warnings in Visual Studio specific to Stardew Valley. For example: +![](screenshots/code-analyzer-example.png) + +You can hide the warnings using the warning ID (shown under 'code' in the Error List). See... +* [for specific code](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/preprocessor-directives/preprocessor-pragma-warning); +* for a method using this attribute: + ```cs + [System.Diagnostics.CodeAnalysis.SuppressMessage("SMAPI.CommonErrors", "AvoidNetField")] + ``` +* for an entire project: + 1. Expand the _References_ node for the project in Visual Studio. + 2. Right-click on _Analyzers_ and choose _Open Active Rule Set_. + 4. Expand _StardewModdingAPI.ModBuildConfig.Analyzer_ and uncheck the warnings you want to hide. + +See below for help with each specific warning. + +### Avoid implicit net field cast +Warning text: +> This implicitly converts '{{expression}}' from {{net type}} to {{other type}}, but +> {{net type}} has unintuitive implicit conversion rules. Consider comparing against the actual +> value instead to avoid bugs. + +Stardew Valley uses net types (like `NetBool` and `NetInt`) to handle multiplayer sync. These types +can implicitly convert to their equivalent normal values (like `bool x = new NetBool()`), but their +conversion rules are unintuitive and error-prone. For example, +`item?.category == null && item?.category != null` can both be true at once, and +`building.indoors != null` can be true for a null value. + +Suggested fix: +* Some net fields have an equivalent non-net property like `monster.Health` (`int`) instead of + `monster.health` (`NetInt`). The package will add a separate [AvoidNetField](#avoid-net-field) warning for + these. Use the suggested property instead. +* For a reference type (i.e. one that can contain `null`), you can use the `.Value` property: + ```c# + if (building.indoors.Value == null) + ``` + Or convert the value before comparison: + ```c# + GameLocation indoors = building.indoors; + if(indoors == null) + // ... + ``` +* For a value type (i.e. one that can't contain `null`), check if the object is null (if applicable) + and compare with `.Value`: + ```cs + if (item != null && item.category.Value == 0) + ``` + +### Avoid net field +Warning text: +> '{{expression}}' is a {{net type}} field; consider using the {{property name}} property instead. + +Your code accesses a net field, which has some unusual behavior (see [AvoidImplicitNetFieldCast](#avoid-implicit-net-field-cast)). +This field has an equivalent non-net property that avoids those issues. + +Suggested fix: access the suggested property name instead. + +### Avoid obsolete field +Warning text: +> The '{{old field}}' field is obsolete and should be replaced with '{{new field}}'. + +Your code accesses a field which is obsolete or no longer works. Use the suggested field instead. + +## Special cases +### Custom game path +The package usually detects where your game is installed automatically. If it can't find your game +or you have multiple installs, you can specify the path yourself. There's two ways to do that: + +* **Option 1: global game path (recommended).** + _This will apply to every project that uses the package._ + + 1. Get the full folder path containing the Stardew Valley executable. + 2. Create this file: + + platform | path + --------- | ---- + Linux/Mac | `~/stardewvalley.targets` + Windows | `%USERPROFILE%\stardewvalley.targets` + + 3. Save the file with this content: + + ```xml + + + PATH_HERE + + + ``` + + 4. Replace `PATH_HERE` with your game path. + +* **Option 2: path in the project file.** + _You'll need to do this for each project that uses the package._ + + 1. Get the folder path containing the Stardew Valley `.exe` file. + 2. Add this to your `.csproj` file under the ` + PATH_HERE + + ``` + + 3. Replace `PATH_HERE` with your custom game install path. + +The configuration will check your custom path first, then fall back to the default paths (so it'll +still compile on a different computer). + +You access the game path via `$(GamePath)` in MSBuild properties, if you need to reference another +file in the game folder. + +### Non-mod projects +You can use the package in non-mod projects too (e.g. unit tests or framework DLLs). Just disable +the mod-related package features: + +```xml +false +false +false +``` + +If you need to copy the referenced DLLs into your build output, add this too: +```xml +true +``` + +## For SMAPI developers +The mod build package consists of three projects: + +project | purpose +------------------------------------------------- | ---------------- +`StardewModdingAPI.ModBuildConfig` | Configures the build (references, deploying the mod files, setting up debugging, etc). +`StardewModdingAPI.ModBuildConfig.Analyzer` | Adds C# analyzers which show code warnings in Visual Studio. +`StardewModdingAPI.ModBuildConfig.Analyzer.Tests` | Unit tests for the C# analyzers. + +To prepare a build of the NuGet package: +1. Install the [NuGet CLI](https://docs.microsoft.com/en-us/nuget/install-nuget-client-tools#nugetexe-cli). +1. Change the version and release notes in `package.nuspec`. +2. Rebuild the solution in _Release_ mode. +3. Open a terminal in the `bin/Pathoschild.Stardew.ModBuildConfig` package and run this command: + ```bash + nuget.exe pack + ``` + +That will create a `Pathoschild.Stardew.ModBuildConfig-.nupkg` file in the same directory +which can be uploaded to NuGet or referenced directly. + +## Release notes +### Upcoming release +* Updated for SMAPI 3.0 and Stardew Valley 1.4. +* Added automatic support for `assets` folders. +* Added `$(GameExecutableName)` MSBuild variable. +* Added support for projects using the simplified `.csproj` format: + * platform target is now set to x86 automatically to avoid mismatching platform target warnings; + * added GAC to assembly search paths to fix references to XNA Framework. +* Added option to disable game debugging config. +* Added `.pdb` files to builds by default (to enable line numbers in error stack traces). +* Added optional Harmony reference. +* Fixed `Newtonsoft.Json.pdb` included in release zips when Json.NET is referenced directly. +* Fixed `` not working for `i18n` files. +* Dropped support for older versions of SMAPI and Visual Studio. + +### 2.2 +* Added support for SMAPI 2.8+ (still compatible with earlier versions). +* Added default game paths for 32-bit Windows. +* Fixed valid manifests marked invalid in some cases. + +### 2.1 +* Added support for Stardew Valley 1.3. +* Added support for non-mod projects. +* Added C# analyzers to warn about implicit conversions of Netcode fields in Stardew Valley 1.3. +* Added option to ignore files by regex pattern. +* Added reference to new SMAPI DLL. +* Fixed some game paths not detected by NuGet package. + +### 2.0.2 +* Fixed compatibility issue on Linux. + +### 2.0.1 +* Fixed mod deploy failing to create subfolders if they don't already exist. + +### 2.0 +* Added: mods are now copied into the `Mods` folder automatically (configurable). +* Added: release zips are now created automatically in your build output folder (configurable). +* Added: mod deploy and release zips now exclude Json.NET automatically, since it's provided by SMAPI. +* Added mod's version to release zip filename. +* Improved errors to simplify troubleshooting. +* Fixed release zip not having a mod folder. +* Fixed release zip failing if mod name contains characters that aren't valid in a filename. + +### 1.7.1 +* Fixed issue where i18n folders were flattened. +* The manifest/i18n files in the project now take precedence over those in the build output if both + are present. + +### 1.7 +* Added option to create release zips on build. +* Added reference to XNA's XACT library for audio-related mods. + +### 1.6 +* Added support for deploying mod files into `Mods` automatically. +* Added a build error if a game folder is found, but doesn't contain Stardew Valley or SMAPI. + +### 1.5 +* Added support for setting a custom game path globally. +* Added default GOG path on Mac. + +### 1.4 +* Fixed detection of non-default game paths on 32-bit Windows. +* Removed support for SilVerPLuM (discontinued). +* Removed support for overriding the target platform (no longer needed since SMAPI crossplatforms + mods automatically). + +### 1.3 +* Added support for non-default game paths on Windows. + +### 1.2 +* Exclude game binaries from mod build output. + +### 1.1 +* Added support for overriding the target platform. + +### 1.0 +* Initial release. +* Added support for detecting the game path automatically. +* Added support for injecting XNA/MonoGame references automatically based on the OS. +* Added support for mod builders like SilVerPLuM. diff --git a/docs/technical/screenshots/code-analyzer-example.png b/docs/technical/screenshots/code-analyzer-example.png new file mode 100644 index 00000000..de38f643 Binary files /dev/null and b/docs/technical/screenshots/code-analyzer-example.png differ diff --git a/docs/technical/smapi.md b/docs/technical/smapi.md new file mode 100644 index 00000000..a006ff1a --- /dev/null +++ b/docs/technical/smapi.md @@ -0,0 +1,105 @@ +← [README](../README.md) + +This file provides more technical documentation about SMAPI. If you only want to use or create +mods, this section isn't relevant to you; see the main README to use or create mods. + +This document is about SMAPI itself; see also [mod build package](mod-package.md) and +[web services](web.md). + +# Contents +* [Customisation](#customisation) + * [Configuration file](#configuration-file) + * [Command-line arguments](#command-line-arguments) + * [Compile flags](#compile-flags) +* [For SMAPI developers](#for-smapi-developers) + * [Compiling from source](#compiling-from-source) + * [Debugging a local build](#debugging-a-local-build) + * [Preparing a release](#preparing-a-release) +* [Release notes](#release-notes) + +## Customisation +### Configuration file +You can customise the SMAPI behaviour by editing the `smapi-internal/config.json` file in your game +folder. + +Basic fields: + +field | purpose +----------------- | ------- +`DeveloperMode` | Default `false` (except in _SMAPI for developers_ releases). Whether to enable features intended for mod developers (mainly more detailed console logging). +`CheckForUpdates` | Default `true`. Whether SMAPI should check for a newer version when you load the game. If a new version is available, a small message will appear in the console. This doesn't affect the load time even if your connection is offline or slow, because it happens in the background. +`VerboseLogging` | Default `false`. Whether SMAPI should log more information about the game context. +`ModData` | Internal metadata about SMAPI mods. Changing this isn't recommended and may destabilise your game. See documentation in the file. + +### Command-line arguments +The SMAPI installer recognises three command-line arguments: + +argument | purpose +-------- | ------- +`--install` | Preselects the install action, skipping the prompt asking what the user wants to do. +`--uninstall` | Preselects the uninstall action, skipping the prompt asking what the user wants to do. +`--game-path "path"` | Specifies the full path to the folder containing the Stardew Valley executable, skipping automatic detection and any prompt to choose a path. If the path is not valid, the installer displays an error. + +SMAPI itself recognises two arguments, but these are intended for internal use or testing and may +change without warning. + +argument | purpose +-------- | ------- +`--no-terminal` | SMAPI won't write anything to the console window. (Messages will still be written to the log file.) +`--mods-path` | The path to search for mods, if not the standard `Mods` folder. This can be a path relative to the game folder (like `--mods-path "Mods (test)"`) or an absolute path. + +### Compile flags +SMAPI uses a small number of conditional compilation constants, which you can set by editing the +`` element in `StardewModdingAPI.csproj`. Supported constants: + +flag | purpose +---- | ------- +`SMAPI_FOR_WINDOWS` | Whether SMAPI is being compiled on Windows for players on Windows. Set automatically in `crossplatform.targets`. + +## For SMAPI developers +### Compiling from source +Using an official SMAPI release is recommended for most users. + +SMAPI uses some C# 7 code, so you'll need at least +[Visual Studio 2017](https://www.visualstudio.com/vs/community/) on Windows, +[MonoDevelop 7.0](https://www.monodevelop.com/) on Linux, +[Visual Studio 2017 for Mac](https://www.visualstudio.com/vs/visual-studio-mac/), or an equivalent +IDE to compile it. It uses build configuration derived from the +[crossplatform mod config](https://github.com/Pathoschild/Stardew.ModBuildConfig#readme) to detect +your current OS automatically and load the correct references. Compile output will be placed in a +`bin` folder at the root of the git repository. + +### Debugging a local build +Rebuilding the solution in debug mode will copy the SMAPI files into your game folder. Starting +the `StardewModdingAPI` project with debugging from Visual Studio (on Mac or Windows) will launch +SMAPI with the debugger attached, so you can intercept errors and step through the code being +executed. This doesn't work in MonoDevelop on Linux, unfortunately. + +### Preparing a release +To prepare a crossplatform SMAPI release, you'll need to compile it on two platforms. See +[crossplatforming info](https://stardewvalleywiki.com/Modding:Modder_Guide/Test_and_Troubleshoot#Testing_on_all_platforms) +on the wiki for the first-time setup. + +1. Update the version number in `GlobalAssemblyInfo.cs` and `Constants::Version`. Make sure you use a + [semantic version](https://semver.org). Recommended format: + + build type | format | example + :--------- | :----------------------- | :------ + dev build | `-alpha.` | `3.0-alpha.20171230` + prerelease | `-beta.` | `3.0-beta.2` + release | `` | `3.0` + +2. In Windows: + 1. Rebuild the solution in Release mode. + 2. Copy `windows-install.*` from `bin/SMAPI installer` and `bin/SMAPI installer for developers` to + Linux/Mac. + +3. In Linux/Mac: + 1. Rebuild the solution in Release mode. + 2. Add the `windows-install.*` files to the `bin/SMAPI installer` and + `bin/SMAPI installer for developers` folders. + 3. Rename the folders to `SMAPI installer` and `SMAPI installer for developers`. + 4. Zip the two folders. + +## Release notes +See [release notes](../release-notes.md). diff --git a/docs/technical/web.md b/docs/technical/web.md new file mode 100644 index 00000000..c8888623 --- /dev/null +++ b/docs/technical/web.md @@ -0,0 +1,106 @@ +← [README](../README.md) + +**SMAPI.Web** contains the code for the `smapi.io` website, including the mod compatibility list +and update check API. + +## Contents +* [Overview](#overview) + * [Log parser](#log-parser) + * [Web API](#web-api) +* [For SMAPI developers](#for-smapi-developers) + * [Local development](#local-development) + * [Deploying to Amazon Beanstalk](#deploying-to-amazon-beanstalk) + +## Overview +The `StardewModdingAPI.Web` project provides an API and web UI hosted at `*.smapi.io`. + +### Log parser +The log parser provides a web UI for uploading, parsing, and sharing SMAPI logs. The logs are +persisted in a compressed form to Pastebin. + +The log parser lives at https://log.smapi.io. + +### Web API +SMAPI provides a web API at `api.smapi.io` for use by SMAPI and external tools. The URL includes a +`{version}` token, which is the SMAPI version for backwards compatibility. This API is publicly +accessible but not officially released; it may change at any time. + +The API has one `/mods` endpoint. This provides mod info, including official versions and URLs +(from Chucklefish, GitHub, or Nexus), unofficial versions from the wiki, and optional mod metadata +from the wiki and SMAPI's internal data. This is used by SMAPI to perform update checks, and by +external tools to fetch mod data. + +The API accepts a `POST` request with the mods to match, each of which **must** specify an ID and +may _optionally_ specify [update keys](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest#Update_checks). +The API will automatically try to fetch known update keys from the wiki and internal data based on +the given ID. + +``` +POST https://api.smapi.io/v2.0/mods +{ + "mods": [ + { + "id": "Pathoschild.LookupAnything", + "updateKeys": [ "nexus:541", "chucklefish:4250" ] + } + ], + "includeExtendedMetadata": true +} +``` + +The API will automatically aggregate versions and errors. Each mod will include... +* an `id` (matching what you passed in); +* up to three versions: `main` (e.g. 'latest version' field on Nexus), `optional` if newer (e.g. + optional files on Nexus), and `unofficial` if newer (from the wiki); +* `metadata` with mod info crossreferenced from the wiki and internal data (only if you specified + `includeExtendedMetadata: true`); +* and `errors` containing any error messages that occurred while fetching data. + +For example: +``` +[ + { + "id": "Pathoschild.LookupAnything", + "main": { + "version": "1.19", + "url": "https://www.nexusmods.com/stardewvalley/mods/541" + }, + "metadata": { + "id": [ + "Pathoschild.LookupAnything", + "LookupAnything" + ], + "name": "Lookup Anything", + "nexusID": 541, + "gitHubRepo": "Pathoschild/StardewMods", + "compatibilityStatus": "Ok", + "compatibilitySummary": "✓ use latest version." + }, + "errors": [] + } +] +``` + +## For SMAPI developers +### Local development +`StardewModdingAPI.Web` is a regular ASP.NET MVC Core app, so you can just launch it from within +Visual Studio to run a local version. + +There are two differences when it's run locally: all endpoints use HTTP instead of HTTPS, and the +subdomain portion becomes a route (e.g. `log.smapi.io` → `localhost:59482/log`). + +Before running it locally, you need to enter your credentials in the `appsettings.Development.json` +file. See the next section for a description of each setting. This file is listed in `.gitignore` +to prevent accidentally committing credentials. + +### Deploying to Amazon Beanstalk +The app can be deployed to a standard Amazon Beanstalk IIS environment. When creating the +environment, make sure to specify the following environment properties: + +property name | description +------------------------------- | ----------------- +`LogParser:PastebinDevKey` | The [Pastebin developer key](https://pastebin.com/api#1) used to authenticate with the Pastebin API. +`LogParser:PastebinUserKey` | The [Pastebin user key](https://pastebin.com/api#8) used to authenticate with the Pastebin API. Can be left blank to post anonymously. +`LogParser:SectionUrl` | The root URL of the log page, like `https://log.smapi.io/`. +`ModUpdateCheck:GitHubPassword` | The password with which to authenticate to GitHub when fetching release info. +`ModUpdateCheck:GitHubUsername` | The username with which to authenticate to GitHub when fetching release info. diff --git a/docs/web-services.md b/docs/web-services.md deleted file mode 100644 index 601152de..00000000 --- a/docs/web-services.md +++ /dev/null @@ -1,104 +0,0 @@ -**SMAPI.Web** contains the code for the `smapi.io` website, including the mod compatibility list -and update check API. - -## Contents -* [Overview](#overview) - * [Log parser](#log-parser) - * [Web API](#web-api) -* [For SMAPI developers](#for-smapi-developers) - * [Local development](#local-development) - * [Deploying to Amazon Beanstalk](#deploying-to-amazon-beanstalk) - -## Overview -The `StardewModdingAPI.Web` project provides an API and web UI hosted at `*.smapi.io`. - -### Log parser -The log parser provides a web UI for uploading, parsing, and sharing SMAPI logs. The logs are -persisted in a compressed form to Pastebin. - -The log parser lives at https://log.smapi.io. - -### Web API -SMAPI provides a web API at `api.smapi.io` for use by SMAPI and external tools. The URL includes a -`{version}` token, which is the SMAPI version for backwards compatibility. This API is publicly -accessible but not officially released; it may change at any time. - -The API has one `/mods` endpoint. This provides mod info, including official versions and URLs -(from Chucklefish, GitHub, or Nexus), unofficial versions from the wiki, and optional mod metadata -from the wiki and SMAPI's internal data. This is used by SMAPI to perform update checks, and by -external tools to fetch mod data. - -The API accepts a `POST` request with the mods to match, each of which **must** specify an ID and -may _optionally_ specify [update keys](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest#Update_checks). -The API will automatically try to fetch known update keys from the wiki and internal data based on -the given ID. - -``` -POST https://api.smapi.io/v2.0/mods -{ - "mods": [ - { - "id": "Pathoschild.LookupAnything", - "updateKeys": [ "nexus:541", "chucklefish:4250" ] - } - ], - "includeExtendedMetadata": true -} -``` - -The API will automatically aggregate versions and errors. Each mod will include... -* an `id` (matching what you passed in); -* up to three versions: `main` (e.g. 'latest version' field on Nexus), `optional` if newer (e.g. - optional files on Nexus), and `unofficial` if newer (from the wiki); -* `metadata` with mod info crossreferenced from the wiki and internal data (only if you specified - `includeExtendedMetadata: true`); -* and `errors` containing any error messages that occurred while fetching data. - -For example: -``` -[ - { - "id": "Pathoschild.LookupAnything", - "main": { - "version": "1.19", - "url": "https://www.nexusmods.com/stardewvalley/mods/541" - }, - "metadata": { - "id": [ - "Pathoschild.LookupAnything", - "LookupAnything" - ], - "name": "Lookup Anything", - "nexusID": 541, - "gitHubRepo": "Pathoschild/StardewMods", - "compatibilityStatus": "Ok", - "compatibilitySummary": "✓ use latest version." - }, - "errors": [] - } -] -``` - -## For SMAPI developers -### Local development -`StardewModdingAPI.Web` is a regular ASP.NET MVC Core app, so you can just launch it from within -Visual Studio to run a local version. - -There are two differences when it's run locally: all endpoints use HTTP instead of HTTPS, and the -subdomain portion becomes a route (e.g. `log.smapi.io` → `localhost:59482/log`). - -Before running it locally, you need to enter your credentials in the `appsettings.Development.json` -file. See the next section for a description of each setting. This file is listed in `.gitignore` -to prevent accidentally committing credentials. - -### Deploying to Amazon Beanstalk -The app can be deployed to a standard Amazon Beanstalk IIS environment. When creating the -environment, make sure to specify the following environment properties: - -property name | description -------------------------------- | ----------------- -`LogParser:PastebinDevKey` | The [Pastebin developer key](https://pastebin.com/api#1) used to authenticate with the Pastebin API. -`LogParser:PastebinUserKey` | The [Pastebin user key](https://pastebin.com/api#8) used to authenticate with the Pastebin API. Can be left blank to post anonymously. -`LogParser:SectionUrl` | The root URL of the log page, like `https://log.smapi.io/`. -`ModUpdateCheck:GitHubPassword` | The password with which to authenticate to GitHub when fetching release info. -`ModUpdateCheck:GitHubUsername` | The username with which to authenticate to GitHub when fetching release info. diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/NetFieldAnalyzerTests.cs b/src/SMAPI.ModBuildConfig.Analyzer.Tests/NetFieldAnalyzerTests.cs index 85a77d15..89bd1be5 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/NetFieldAnalyzerTests.cs +++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/NetFieldAnalyzerTests.cs @@ -96,7 +96,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests DiagnosticResult expected = new DiagnosticResult { Id = "AvoidImplicitNetFieldCast", - Message = $"This implicitly converts '{expression}' from {fromType} to {toType}, but {fromType} has unintuitive implicit conversion rules. Consider comparing against the actual value instead to avoid bugs. See https://smapi.io/buildmsg/avoid-implicit-net-field-cast for details.", + Message = $"This implicitly converts '{expression}' from {fromType} to {toType}, but {fromType} has unintuitive implicit conversion rules. Consider comparing against the actual value instead to avoid bugs. See https://smapi.io/package/avoid-implicit-net-field-cast for details.", Severity = DiagnosticSeverity.Warning, Locations = new[] { new DiagnosticResultLocation("Test0.cs", NetFieldAnalyzerTests.SampleCodeLine, NetFieldAnalyzerTests.SampleCodeColumn + column) } }; @@ -138,7 +138,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests DiagnosticResult expected = new DiagnosticResult { Id = "AvoidNetField", - Message = $"'{expression}' is a {netType} field; consider using the {suggestedProperty} property instead. See https://smapi.io/buildmsg/avoid-net-field for details.", + Message = $"'{expression}' is a {netType} field; consider using the {suggestedProperty} property instead. See https://smapi.io/package/avoid-net-field for details.", Severity = DiagnosticSeverity.Warning, Locations = new[] { new DiagnosticResultLocation("Test0.cs", NetFieldAnalyzerTests.SampleCodeLine, NetFieldAnalyzerTests.SampleCodeColumn + column) } }; diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/ObsoleteFieldAnalyzerTests.cs b/src/SMAPI.ModBuildConfig.Analyzer.Tests/ObsoleteFieldAnalyzerTests.cs index fa9235a3..12641e1a 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/ObsoleteFieldAnalyzerTests.cs +++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/ObsoleteFieldAnalyzerTests.cs @@ -67,7 +67,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests DiagnosticResult expected = new DiagnosticResult { Id = "AvoidObsoleteField", - Message = $"The '{oldName}' field is obsolete and should be replaced with '{newName}'. See https://smapi.io/buildmsg/avoid-obsolete-field for details.", + Message = $"The '{oldName}' field is obsolete and should be replaced with '{newName}'. See https://smapi.io/package/avoid-obsolete-field for details.", Severity = DiagnosticSeverity.Warning, Locations = new[] { new DiagnosticResultLocation("Test0.cs", ObsoleteFieldAnalyzerTests.SampleCodeLine, ObsoleteFieldAnalyzerTests.SampleCodeColumn + column) } }; diff --git a/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs b/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs index f2608348..70c01418 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs +++ b/src/SMAPI.ModBuildConfig.Analyzer/NetFieldAnalyzer.cs @@ -135,22 +135,22 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer private readonly DiagnosticDescriptor AvoidImplicitNetFieldCastRule = new DiagnosticDescriptor( id: "AvoidImplicitNetFieldCast", title: "Netcode types shouldn't be implicitly converted", - messageFormat: "This implicitly converts '{0}' from {1} to {2}, but {1} has unintuitive implicit conversion rules. Consider comparing against the actual value instead to avoid bugs. See https://smapi.io/buildmsg/avoid-implicit-net-field-cast for details.", + messageFormat: "This implicitly converts '{0}' from {1} to {2}, but {1} has unintuitive implicit conversion rules. Consider comparing against the actual value instead to avoid bugs. See https://smapi.io/package/avoid-implicit-net-field-cast for details.", category: "SMAPI.CommonErrors", defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, - helpLinkUri: "https://smapi.io/buildmsg/avoid-implicit-net-field-cast" + helpLinkUri: "https://smapi.io/package/avoid-implicit-net-field-cast" ); /// The diagnostic info for an avoidable net field access. private readonly DiagnosticDescriptor AvoidNetFieldRule = new DiagnosticDescriptor( id: "AvoidNetField", title: "Avoid Netcode types when possible", - messageFormat: "'{0}' is a {1} field; consider using the {2} property instead. See https://smapi.io/buildmsg/avoid-net-field for details.", + messageFormat: "'{0}' is a {1} field; consider using the {2} property instead. See https://smapi.io/package/avoid-net-field for details.", category: "SMAPI.CommonErrors", defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, - helpLinkUri: "https://smapi.io/buildmsg/avoid-net-field" + helpLinkUri: "https://smapi.io/package/avoid-net-field" ); diff --git a/src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs b/src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs index f1a3ef75..6b935caa 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs +++ b/src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs @@ -27,11 +27,11 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer ["AvoidObsoleteField"] = new DiagnosticDescriptor( id: "AvoidObsoleteField", title: "Reference to obsolete field", - messageFormat: "The '{0}' field is obsolete and should be replaced with '{1}'. See https://smapi.io/buildmsg/avoid-obsolete-field for details.", + messageFormat: "The '{0}' field is obsolete and should be replaced with '{1}'. See https://smapi.io/package/avoid-obsolete-field for details.", category: "SMAPI.CommonErrors", defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, - helpLinkUri: "https://smapi.io/buildmsg/avoid-obsolete-field" + helpLinkUri: "https://smapi.io/package/avoid-obsolete-field" ) }; diff --git a/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj b/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj index 70636937..e13526f9 100644 --- a/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj +++ b/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj @@ -16,9 +16,7 @@ - - mod-build-config.md - + diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets index 51432d96..5eeea91e 100644 --- a/src/SMAPI.ModBuildConfig/build/smapi.targets +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -107,7 +107,7 @@ - + diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec index 6779798d..ffa47fdb 100644 --- a/src/SMAPI.ModBuildConfig/package.nuspec +++ b/src/SMAPI.ModBuildConfig/package.nuspec @@ -9,9 +9,9 @@ false MIT - https://github.com/Pathoschild/SMAPI/blob/develop/docs/mod-build-config.md#readme + https://smapi.io/package/readme https://raw.githubusercontent.com/Pathoschild/SMAPI/develop/src/SMAPI.ModBuildConfig/assets/nuget-icon.png - Automates the build configuration for crossplatform Stardew Valley SMAPI mods. For SMAPI 2.11 or later. + Automates the build configuration for crossplatform Stardew Valley SMAPI mods. For SMAPI 3.0 or later. 3.0.0: - Updated for SMAPI 3.0 and Stardew Valley 1.4. diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index a2e47482..b409a81d 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -162,7 +162,7 @@ namespace StardewModdingAPI.Web // shortcut redirects redirects.Add(new RedirectToUrlRule(@"^/3\.0\.?$", "https://stardewvalleywiki.com/Modding:Migrate_to_SMAPI_3.0")); - redirects.Add(new RedirectToUrlRule(@"^/buildmsg(?:/?(.*))$", "https://github.com/Pathoschild/SMAPI/blob/develop/docs/mod-build-config.md#$1")); + redirects.Add(new RedirectToUrlRule(@"^/(?:buildmsg|package)(?:/?(.*))$", "https://github.com/Pathoschild/SMAPI/blob/develop/docs/technical/mod-package.md#$1")); // buildmsg deprecated, remove when SDV 1.4 is released redirects.Add(new RedirectToUrlRule(@"^/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.sln b/src/SMAPI.sln index 08a47a46..8ee91b11 100644 --- a/src/SMAPI.sln +++ b/src/SMAPI.sln @@ -35,11 +35,15 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{09CF91E5 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{EB35A917-67B9-4EFA-8DFC-4FB49B3949BB}" ProjectSection(SolutionItems) = preProject - ..\docs\mod-build-config.md = ..\docs\mod-build-config.md ..\docs\README.md = ..\docs\README.md ..\docs\release-notes.md = ..\docs\release-notes.md - ..\docs\technical-docs.md = ..\docs\technical-docs.md - ..\docs\web-services.md = ..\docs\web-services.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "technical", "technical", "{5947303D-3512-413A-9009-7AC43F5D3513}" + ProjectSection(SolutionItems) = preProject + ..\docs\technical\mod-package.md = ..\docs\technical\mod-package.md + ..\docs\technical\smapi.md = ..\docs\technical\smapi.md + ..\docs\technical\web.md = ..\docs\technical\web.md EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Internal", "Internal", "{82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11}" @@ -135,6 +139,7 @@ Global {F4453AB6-D7D6-447F-A973-956CC777968F} = {4B1CEB70-F756-4A57-AAE8-8CD78C475F25} {09CF91E5-5BAB-4650-A200-E5EA9A633046} = {86C452BE-D2D8-45B4-B63F-E329EB06CEDA} {EB35A917-67B9-4EFA-8DFC-4FB49B3949BB} = {86C452BE-D2D8-45B4-B63F-E329EB06CEDA} + {5947303D-3512-413A-9009-7AC43F5D3513} = {EB35A917-67B9-4EFA-8DFC-4FB49B3949BB} {85208F8D-6FD1-4531-BE05-7142490F59FE} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} {680B2641-81EA-467C-86A5-0E81CDC57ED0} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} {AA95884B-7097-476E-92C8-D0500DE9D6D1} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} -- cgit From 9162f41462ad91e684260212141e1aeba0625933 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 6 Jul 2019 18:22:55 -0400 Subject: add redirect for old links until NuGet package is updated (#651) --- docs/mod-build-config.md | 1 + src/SMAPI.sln | 1 + 2 files changed, 2 insertions(+) create mode 100644 docs/mod-build-config.md (limited to 'src') diff --git a/docs/mod-build-config.md b/docs/mod-build-config.md new file mode 100644 index 00000000..4ec83e93 --- /dev/null +++ b/docs/mod-build-config.md @@ -0,0 +1 @@ +[Documentation moved](technical/mod-package.md). diff --git a/src/SMAPI.sln b/src/SMAPI.sln index 8ee91b11..73852d30 100644 --- a/src/SMAPI.sln +++ b/src/SMAPI.sln @@ -35,6 +35,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{09CF91E5 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{EB35A917-67B9-4EFA-8DFC-4FB49B3949BB}" ProjectSection(SolutionItems) = preProject + ..\docs\mod-build-config.md = ..\docs\mod-build-config.md ..\docs\README.md = ..\docs\README.md ..\docs\release-notes.md = ..\docs\release-notes.md EndProjectSection -- cgit From f18ad1210cd813d6ddff665841ac712d62d18b1f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 6 Jul 2019 21:10:00 -0400 Subject: update project name --- docs/technical/web.md | 4 ++-- src/SMAPI.Toolkit/Properties/AssemblyInfo.cs | 2 +- src/SMAPI.Web/SMAPI.Web.csproj | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/docs/technical/web.md b/docs/technical/web.md index c8888623..978114ef 100644 --- a/docs/technical/web.md +++ b/docs/technical/web.md @@ -12,7 +12,7 @@ and update check API. * [Deploying to Amazon Beanstalk](#deploying-to-amazon-beanstalk) ## Overview -The `StardewModdingAPI.Web` project provides an API and web UI hosted at `*.smapi.io`. +The `SMAPI.Web` project provides an API and web UI hosted at `*.smapi.io`. ### Log parser The log parser provides a web UI for uploading, parsing, and sharing SMAPI logs. The logs are @@ -83,7 +83,7 @@ For example: ## For SMAPI developers ### Local development -`StardewModdingAPI.Web` is a regular ASP.NET MVC Core app, so you can just launch it from within +`SMAPI.Web` is a regular ASP.NET MVC Core app, so you can just launch it from within Visual Studio to run a local version. There are two differences when it's run locally: all endpoints use HTTP instead of HTTPS, and the diff --git a/src/SMAPI.Toolkit/Properties/AssemblyInfo.cs b/src/SMAPI.Toolkit/Properties/AssemblyInfo.cs index 1bb19e8c..ec873f79 100644 --- a/src/SMAPI.Toolkit/Properties/AssemblyInfo.cs +++ b/src/SMAPI.Toolkit/Properties/AssemblyInfo.cs @@ -4,4 +4,4 @@ using System.Runtime.CompilerServices; [assembly: AssemblyTitle("SMAPI.Toolkit")] [assembly: AssemblyDescription("A library which encapsulates mod-handling logic for mod managers and tools. Not intended for use by mods.")] [assembly: InternalsVisibleTo("StardewModdingAPI")] -[assembly: InternalsVisibleTo("StardewModdingAPI.Web")] +[assembly: InternalsVisibleTo("SMAPI.Web")] diff --git a/src/SMAPI.Web/SMAPI.Web.csproj b/src/SMAPI.Web/SMAPI.Web.csproj index 1d8e41c6..b0aaa8a9 100644 --- a/src/SMAPI.Web/SMAPI.Web.csproj +++ b/src/SMAPI.Web/SMAPI.Web.csproj @@ -1,6 +1,7 @@  + StardewModdingAPI.Web netcoreapp2.0 false latest -- cgit From e00fb85ee7822bc7fed2d6bd5a2e4c207a799418 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 7 Jul 2019 00:29:22 -0400 Subject: migrate compatibility list's wiki data to MongoDB cache (#651) --- docs/technical/web.md | 68 +++++--- src/SMAPI.Web/Controllers/ModsController.cs | 37 ++-- .../Framework/Caching/BaseCacheRepository.cs | 19 +++ .../Framework/Caching/Wiki/CachedWikiMetadata.cs | 43 +++++ .../Framework/Caching/Wiki/CachedWikiMod.cs | 188 +++++++++++++++++++++ .../Framework/Caching/Wiki/IWikiCacheRepository.cs | 35 ++++ .../Framework/Caching/Wiki/WikiCacheRepository.cs | 73 ++++++++ .../Framework/ConfigModels/MongoDbConfig.cs | 36 ++++ src/SMAPI.Web/SMAPI.Web.csproj | 4 + src/SMAPI.Web/Startup.cs | 12 ++ src/SMAPI.Web/appsettings.Development.json | 6 + src/SMAPI.Web/appsettings.json | 6 + 12 files changed, 487 insertions(+), 40 deletions(-) create mode 100644 src/SMAPI.Web/Framework/Caching/BaseCacheRepository.cs create mode 100644 src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMetadata.cs create mode 100644 src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs create mode 100644 src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs create mode 100644 src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheRepository.cs create mode 100644 src/SMAPI.Web/Framework/ConfigModels/MongoDbConfig.cs (limited to 'src') diff --git a/docs/technical/web.md b/docs/technical/web.md index 978114ef..db54a87a 100644 --- a/docs/technical/web.md +++ b/docs/technical/web.md @@ -82,25 +82,49 @@ For example: ``` ## For SMAPI developers -### Local development -`SMAPI.Web` is a regular ASP.NET MVC Core app, so you can just launch it from within -Visual Studio to run a local version. - -There are two differences when it's run locally: all endpoints use HTTP instead of HTTPS, and the -subdomain portion becomes a route (e.g. `log.smapi.io` → `localhost:59482/log`). - -Before running it locally, you need to enter your credentials in the `appsettings.Development.json` -file. See the next section for a description of each setting. This file is listed in `.gitignore` -to prevent accidentally committing credentials. - -### Deploying to Amazon Beanstalk -The app can be deployed to a standard Amazon Beanstalk IIS environment. When creating the -environment, make sure to specify the following environment properties: - -property name | description -------------------------------- | ----------------- -`LogParser:PastebinDevKey` | The [Pastebin developer key](https://pastebin.com/api#1) used to authenticate with the Pastebin API. -`LogParser:PastebinUserKey` | The [Pastebin user key](https://pastebin.com/api#8) used to authenticate with the Pastebin API. Can be left blank to post anonymously. -`LogParser:SectionUrl` | The root URL of the log page, like `https://log.smapi.io/`. -`ModUpdateCheck:GitHubPassword` | The password with which to authenticate to GitHub when fetching release info. -`ModUpdateCheck:GitHubUsername` | The username with which to authenticate to GitHub when fetching release info. +### Local environment +A local environment lets you run a complete copy of the web project (including cache database) on +your machine, with no external dependencies aside from the actual mod sites. + +Initial setup: + +1. [Install MongoDB](https://docs.mongodb.com/manual/administration/install-community/) and add its + `bin` folder to the system PATH. +2. Create a local folder for the MongoDB data (e.g. `C:\dev\smapi-cache`). +3. Enter your credentials in the `appsettings.Development.json` file. You can leave the MongoDB + credentials as-is to use the default local instance; see the next section for the other settings. + +To launch the environment: +1. Launch MongoDB from a terminal (change the data path if applicable): + ```sh + mongod --dbpath C:\dev\smapi-cache + ``` +2. Launch `SMAPI.Web` from Visual Studio to run a local version of the site. + (Local URLs will use HTTP instead of HTTPS, and subdomains will become routes, like + `log.smapi.io` → `localhost:59482/log`.) + +### Production environment +A production environment includes the web servers and cache database hosted online for public +access. This section assumes you're creating a new production environment from scratch (not using +the official live environment). + +Initial setup: + +1. Launch an empty MongoDB server (e.g. using [MongoDB Atlas](https://www.mongodb.com/cloud/atlas)). +2. Create an AWS Beanstalk .NET environment with these environment properties: + + property name | description + ------------------------------- | ----------------- + `LogParser:PastebinDevKey` | The [Pastebin developer key](https://pastebin.com/api#1) used to authenticate with the Pastebin API. + `LogParser:PastebinUserKey` | The [Pastebin user key](https://pastebin.com/api#8) used to authenticate with the Pastebin API. Can be left blank to post anonymously. + `LogParser:SectionUrl` | The root URL of the log page, like `https://log.smapi.io/`. + `ModUpdateCheck:GitHubPassword` | The password with which to authenticate to GitHub when fetching release info. + `ModUpdateCheck:GitHubUsername` | The username with which to authenticate to GitHub when fetching release info. + `MongoDB:Host` | The hostname for the MongoDB instance. + `MongoDB:Username` | The login username for the MongoDB instance. + `MongoDB:Password` | The login password for the MongoDB instance. + +To deploy updates: +1. Deploy the web project using [AWS Toolkit for Visual Studio](https://aws.amazon.com/visualstudio/). +2. If the MongoDB schema changed, delete the collections in the MongoDB database. (They'll be + recreated automatically.) diff --git a/src/SMAPI.Web/Controllers/ModsController.cs b/src/SMAPI.Web/Controllers/ModsController.cs index ca866a8d..b6040e06 100644 --- a/src/SMAPI.Web/Controllers/ModsController.cs +++ b/src/SMAPI.Web/Controllers/ModsController.cs @@ -1,12 +1,10 @@ -using System; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using StardewModdingAPI.Toolkit; -using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; +using StardewModdingAPI.Web.Framework.Caching.Wiki; using StardewModdingAPI.Web.Framework.ConfigModels; using StardewModdingAPI.Web.ViewModels; @@ -19,7 +17,7 @@ namespace StardewModdingAPI.Web.Controllers ** Fields *********/ /// The cache in which to store mod metadata. - private readonly IMemoryCache Cache; + private readonly IWikiCacheRepository Cache; /// The number of minutes successful update checks should be cached before refetching them. private readonly int CacheMinutes; @@ -31,7 +29,7 @@ namespace StardewModdingAPI.Web.Controllers /// Construct an instance. /// The cache in which to store mod metadata. /// The config settings for mod update checks. - public ModsController(IMemoryCache cache, IOptions configProvider) + public ModsController(IWikiCacheRepository cache, IOptions configProvider) { ModCompatibilityListConfig config = configProvider.Value; @@ -54,21 +52,24 @@ namespace StardewModdingAPI.Web.Controllers /// Asynchronously fetch mod metadata from the wiki. public async Task FetchDataAsync() { - return await this.Cache.GetOrCreateAsync($"{nameof(ModsController)}_mod_list", async entry => + // refresh cache + CachedWikiMod[] mods; + if (!this.Cache.TryGetWikiMetadata(out CachedWikiMetadata metadata) || this.Cache.IsStale(metadata.LastUpdated, this.CacheMinutes)) { - WikiModList data = await new ModToolkit().GetWikiCompatibilityListAsync(); - ModListModel model = new ModListModel( - stableVersion: data.StableVersion, - betaVersion: data.BetaVersion, - mods: data - .Mods - .Select(mod => new ModModel(mod)) - .OrderBy(p => Regex.Replace(p.Name.ToLower(), "[^a-z0-9]", "")) // ignore case, spaces, and special characters when sorting - ); + var wikiCompatList = await new ModToolkit().GetWikiCompatibilityListAsync(); + this.Cache.SaveWikiData(wikiCompatList.StableVersion, wikiCompatList.BetaVersion, wikiCompatList.Mods, out metadata, out mods); + } + else + mods = this.Cache.GetWikiMods().ToArray(); - entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(this.CacheMinutes); - return model; - }); + // build model + return new ModListModel( + stableVersion: metadata.StableVersion, + betaVersion: metadata.BetaVersion, + mods: mods + .Select(mod => new ModModel(mod.GetModel())) + .OrderBy(p => Regex.Replace(p.Name.ToLower(), "[^a-z0-9]", "")) // ignore case, spaces, and special characters when sorting + ); } } } diff --git a/src/SMAPI.Web/Framework/Caching/BaseCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/BaseCacheRepository.cs new file mode 100644 index 00000000..904455c5 --- /dev/null +++ b/src/SMAPI.Web/Framework/Caching/BaseCacheRepository.cs @@ -0,0 +1,19 @@ +using System; + +namespace StardewModdingAPI.Web.Framework.Caching +{ + /// The base logic for a cache repository. + internal abstract class BaseCacheRepository + { + /********* + ** Public methods + *********/ + /// Whether cached data is stale. + /// The date when the data was updated. + /// The age in minutes before data is considered stale. + public bool IsStale(DateTimeOffset lastUpdated, int cacheMinutes) + { + return lastUpdated < DateTimeOffset.UtcNow.AddMinutes(-cacheMinutes); + } + } +} diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMetadata.cs b/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMetadata.cs new file mode 100644 index 00000000..de1ea9db --- /dev/null +++ b/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMetadata.cs @@ -0,0 +1,43 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using MongoDB.Bson; + +namespace StardewModdingAPI.Web.Framework.Caching.Wiki +{ + /// The model for cached wiki metadata. + public class CachedWikiMetadata + { + /********* + ** Accessors + *********/ + /// The internal MongoDB ID. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Named per MongoDB conventions.")] + public ObjectId _id { get; set; } + + /// When the data was last updated. + public DateTimeOffset LastUpdated { get; set; } + + /// The current stable Stardew Valley version. + public string StableVersion { get; set; } + + /// The current beta Stardew Valley version. + public string BetaVersion { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public CachedWikiMetadata() { } + + /// Construct an instance. + /// The current stable Stardew Valley version. + /// The current beta Stardew Valley version. + public CachedWikiMetadata(string stableVersion, string betaVersion) + { + this.StableVersion = stableVersion; + this.BetaVersion = betaVersion; + this.LastUpdated = DateTimeOffset.UtcNow;; + } + } +} diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs b/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs new file mode 100644 index 00000000..f4331f8d --- /dev/null +++ b/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs @@ -0,0 +1,188 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using MongoDB.Bson; +using StardewModdingAPI.Toolkit; +using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; + +namespace StardewModdingAPI.Web.Framework.Caching.Wiki +{ + /// The model for cached wiki mods. + public class CachedWikiMod + { + /********* + ** Accessors + *********/ + /**** + ** Tracking + ****/ + /// The internal MongoDB ID. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Named per MongoDB conventions.")] + public ObjectId _id { get; set; } + + /// When the data was last updated. + public DateTimeOffset LastUpdated { get; set; } + + /**** + ** Mod info + ****/ + /// The mod's unique ID. If the mod has alternate/old IDs, they're listed in latest to newest order. + public string[] ID { get; set; } + + /// The mod's display name. If the mod has multiple names, the first one is the most canonical name. + public string[] Name { get; set; } + + /// The mod's author name. If the author has multiple names, the first one is the most canonical name. + public string[] Author { get; set; } + + /// The mod ID on Nexus. + public int? NexusID { get; set; } + + /// The mod ID in the Chucklefish mod repo. + public int? ChucklefishID { get; set; } + + /// The mod ID in the ModDrop mod repo. + public int? ModDropID { get; set; } + + /// The GitHub repository in the form 'owner/repo'. + public string GitHubRepo { get; set; } + + /// The URL to a non-GitHub source repo. + public string CustomSourceUrl { get; set; } + + /// The custom mod page URL (if applicable). + public string CustomUrl { get; set; } + + /// The name of the mod which loads this content pack, if applicable. + public string ContentPackFor { get; set; } + + /// The human-readable warnings for players about this mod. + public string[] Warnings { get; set; } + + /// The link anchor for the mod entry in the wiki compatibility list. + public string Anchor { get; set; } + + /**** + ** Stable compatibility + ****/ + /// The compatibility status. + public WikiCompatibilityStatus MainStatus { get; set; } + + /// The human-readable summary of the compatibility status or workaround, without HTML formatting. + public string MainSummary { get; set; } + + /// The game or SMAPI version which broke this mod (if applicable). + public string MainBrokeIn { get; set; } + + /// The version of the latest unofficial update, if applicable. + public string MainUnofficialVersion { get; set; } + + /// The URL to the latest unofficial update, if applicable. + public string MainUnofficialUrl { get; set; } + + /**** + ** Beta compatibility + ****/ + /// The compatibility status. + public WikiCompatibilityStatus? BetaStatus { get; set; } + + /// The human-readable summary of the compatibility status or workaround, without HTML formatting. + public string BetaSummary { get; set; } + + /// The game or SMAPI version which broke this mod (if applicable). + public string BetaBrokeIn { get; set; } + + /// The version of the latest unofficial update, if applicable. + public string BetaUnofficialVersion { get; set; } + + /// The URL to the latest unofficial update, if applicable. + public string BetaUnofficialUrl { get; set; } + + + /********* + ** Accessors + *********/ + /// Construct an instance. + public CachedWikiMod() { } + + /// Construct an instance. + /// The mod data. + public CachedWikiMod(WikiModEntry mod) + { + // tracking + this.LastUpdated = DateTimeOffset.UtcNow; + + // mod info + this.ID = mod.ID; + this.Name = mod.Name; + this.Author = mod.Author; + this.NexusID = mod.NexusID; + this.ChucklefishID = mod.ChucklefishID; + this.ModDropID = mod.ModDropID; + this.GitHubRepo = mod.GitHubRepo; + this.CustomSourceUrl = mod.CustomSourceUrl; + this.CustomUrl = mod.CustomUrl; + this.ContentPackFor = mod.ContentPackFor; + this.Warnings = mod.Warnings; + this.Anchor = mod.Anchor; + + // stable compatibility + this.MainStatus = mod.Compatibility.Status; + this.MainSummary = mod.Compatibility.Summary; + this.MainBrokeIn = mod.Compatibility.BrokeIn; + this.MainUnofficialVersion = mod.Compatibility.UnofficialVersion?.ToString(); + this.MainUnofficialUrl = mod.Compatibility.UnofficialUrl; + + // beta compatibility + this.BetaStatus = mod.BetaCompatibility?.Status; + this.BetaSummary = mod.BetaCompatibility?.Summary; + this.BetaBrokeIn = mod.BetaCompatibility?.BrokeIn; + this.BetaUnofficialVersion = mod.BetaCompatibility?.UnofficialVersion?.ToString(); + this.BetaUnofficialUrl = mod.BetaCompatibility?.UnofficialUrl; + } + + /// Reconstruct the original model. + public WikiModEntry GetModel() + { + var mod = new WikiModEntry + { + ID = this.ID, + Name = this.Name, + Author = this.Author, + NexusID = this.NexusID, + ChucklefishID = this.ChucklefishID, + ModDropID = this.ModDropID, + GitHubRepo = this.GitHubRepo, + CustomSourceUrl = this.CustomSourceUrl, + CustomUrl = this.CustomUrl, + ContentPackFor = this.ContentPackFor, + Warnings = this.Warnings, + Anchor = this.Anchor, + + // stable compatibility + Compatibility = new WikiCompatibilityInfo + { + Status = this.MainStatus, + Summary = this.MainSummary, + BrokeIn = this.MainBrokeIn, + UnofficialVersion = this.MainUnofficialVersion != null ? new SemanticVersion(this.MainUnofficialVersion) : null, + UnofficialUrl = this.MainUnofficialUrl + } + }; + + // beta compatibility + if (this.BetaStatus != null) + { + mod.BetaCompatibility = new WikiCompatibilityInfo + { + Status = this.BetaStatus.Value, + Summary = this.BetaSummary, + BrokeIn = this.BetaBrokeIn, + UnofficialVersion = this.BetaUnofficialVersion != null ? new SemanticVersion(this.BetaUnofficialVersion) : null, + UnofficialUrl = this.BetaUnofficialUrl + }; + } + + return mod; + } + } +} diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs new file mode 100644 index 00000000..d319db69 --- /dev/null +++ b/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; + +namespace StardewModdingAPI.Web.Framework.Caching.Wiki +{ + /// Encapsulates logic for accessing the mod data cache. + internal interface IWikiCacheRepository + { + /********* + ** Methods + *********/ + /// Get the cached wiki metadata. + /// The fetched metadata. + bool TryGetWikiMetadata(out CachedWikiMetadata metadata); + + /// Whether cached data is stale. + /// The date when the data was updated. + /// The age in minutes before data is considered stale. + bool IsStale(DateTimeOffset lastUpdated, int cacheMinutes); + + /// Get the cached wiki mods. + /// A filter to apply, if any. + IEnumerable GetWikiMods(Expression> filter = null); + + /// Save data fetched from the wiki compatibility list. + /// The current stable Stardew Valley version. + /// The current beta Stardew Valley version. + /// The mod data. + /// The stored metadata record. + /// The stored mod records. + void SaveWikiData(string stableVersion, string betaVersion, IEnumerable mods, out CachedWikiMetadata cachedMetadata, out CachedWikiMod[] cachedMods); + } +} diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheRepository.cs new file mode 100644 index 00000000..5b907462 --- /dev/null +++ b/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheRepository.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using MongoDB.Driver; +using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; + +namespace StardewModdingAPI.Web.Framework.Caching.Wiki +{ + /// Encapsulates logic for accessing the wiki data cache. + internal class WikiCacheRepository : BaseCacheRepository, IWikiCacheRepository + { + /********* + ** Fields + *********/ + /// The collection for wiki metadata. + private readonly IMongoCollection WikiMetadata; + + /// The collection for wiki mod data. + private readonly IMongoCollection WikiMods; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The authenticated MongoDB database. + public WikiCacheRepository(IMongoDatabase database) + { + // get collections + this.WikiMetadata = database.GetCollection("wiki-metadata"); + this.WikiMods = database.GetCollection("wiki-mods"); + + // add indexes if needed + this.WikiMods.Indexes.CreateOne(new CreateIndexModel(Builders.IndexKeys.Text(p => p.ID))); + } + + /// Get the cached wiki metadata. + /// The fetched metadata. + public bool TryGetWikiMetadata(out CachedWikiMetadata metadata) + { + metadata = this.WikiMetadata.Find("{}").FirstOrDefault(); + return metadata != null; + } + + /// Get the cached wiki mods. + /// A filter to apply, if any. + public IEnumerable GetWikiMods(Expression> filter = null) + { + return filter != null + ? this.WikiMods.Find(filter).ToList() + : this.WikiMods.Find("{}").ToList(); + } + + /// Save data fetched from the wiki compatibility list. + /// The current stable Stardew Valley version. + /// The current beta Stardew Valley version. + /// The mod data. + /// The stored metadata record. + /// The stored mod records. + public void SaveWikiData(string stableVersion, string betaVersion, IEnumerable mods, out CachedWikiMetadata cachedMetadata, out CachedWikiMod[] cachedMods) + { + cachedMetadata = new CachedWikiMetadata(stableVersion, betaVersion); + cachedMods = mods.Select(mod => new CachedWikiMod(mod)).ToArray(); + + this.WikiMods.DeleteMany("{}"); + this.WikiMods.InsertMany(cachedMods); + + this.WikiMetadata.DeleteMany("{}"); + this.WikiMetadata.InsertOne(cachedMetadata); + } + } +} diff --git a/src/SMAPI.Web/Framework/ConfigModels/MongoDbConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/MongoDbConfig.cs new file mode 100644 index 00000000..352eb960 --- /dev/null +++ b/src/SMAPI.Web/Framework/ConfigModels/MongoDbConfig.cs @@ -0,0 +1,36 @@ +using System; + +namespace StardewModdingAPI.Web.Framework.ConfigModels +{ + /// The config settings for mod compatibility list. + internal class MongoDbConfig + { + /********* + ** Accessors + *********/ + /// The MongoDB hostname. + public string Host { get; set; } + + /// The MongoDB username (if any). + public string Username { get; set; } + + /// The MongoDB password (if any). + public string Password { get; set; } + + + /********* + ** Public method + *********/ + /// Get the MongoDB connection string. + /// The initial database for which to authenticate. + public string GetConnectionString(string authDatabase) + { + bool isLocal = this.Host == "localhost"; + bool hasLogin = !string.IsNullOrWhiteSpace(this.Username) && !string.IsNullOrWhiteSpace(this.Password); + + return $"mongodb{(isLocal ? "" : "+srv")}://" + + (hasLogin ? $"{Uri.EscapeDataString(this.Username)}:{Uri.EscapeDataString(this.Password)}@" : "") + + $"{this.Host}/{authDatabase}retryWrites=true&w=majority"; + } + } +} diff --git a/src/SMAPI.Web/SMAPI.Web.csproj b/src/SMAPI.Web/SMAPI.Web.csproj index b0aaa8a9..90641611 100644 --- a/src/SMAPI.Web/SMAPI.Web.csproj +++ b/src/SMAPI.Web/SMAPI.Web.csproj @@ -23,12 +23,16 @@ + + + + diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index b409a81d..0a8d23a8 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -6,9 +6,11 @@ using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using MongoDB.Driver; using Newtonsoft.Json; using StardewModdingAPI.Toolkit.Serialisation; using StardewModdingAPI.Web.Framework; +using StardewModdingAPI.Web.Framework.Caching.Wiki; using StardewModdingAPI.Web.Framework.Clients.Chucklefish; using StardewModdingAPI.Web.Framework.Clients.GitHub; using StardewModdingAPI.Web.Framework.Clients.ModDrop; @@ -53,6 +55,7 @@ namespace StardewModdingAPI.Web .Configure(this.Configuration.GetSection("ModCompatibilityList")) .Configure(this.Configuration.GetSection("ModUpdateCheck")) .Configure(this.Configuration.GetSection("Site")) + .Configure(this.Configuration.GetSection("MongoDB")) .Configure(options => options.ConstraintMap.Add("semanticVersion", typeof(VersionConstraint))) .AddMemoryCache() .AddMvc() @@ -108,6 +111,15 @@ namespace StardewModdingAPI.Web devKey: api.PastebinDevKey )); } + + // init MongoDB + { + MongoDbConfig mongoConfig = this.Configuration.GetSection("MongoDB").Get(); + string connectionString = mongoConfig.GetConnectionString("smapi"); + + services.AddSingleton(serv => new MongoClient(connectionString).GetDatabase("smapi")); + services.AddSingleton(serv => new WikiCacheRepository(serv.GetService())); + } } /// The method called by the runtime to configure the HTTP request pipeline. diff --git a/src/SMAPI.Web/appsettings.Development.json b/src/SMAPI.Web/appsettings.Development.json index 49234a3b..9b0ec535 100644 --- a/src/SMAPI.Web/appsettings.Development.json +++ b/src/SMAPI.Web/appsettings.Development.json @@ -31,5 +31,11 @@ "PastebinUserKey": null, "PastebinDevKey": null + }, + + "MongoDB": { + "Host": "localhost", + "Username": null, + "Password": null } } diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json index 9e15aa97..65ccea75 100644 --- a/src/SMAPI.Web/appsettings.json +++ b/src/SMAPI.Web/appsettings.json @@ -47,6 +47,12 @@ "PastebinDevKey": null // see top note }, + "MongoDB": { + "Host": null, // see top note + "Username": null, // see top note + "Password": null // see top note + }, + "ModCompatibilityList": { "CacheMinutes": 10 }, -- cgit From 2b3f0e740bc09630e881c9967e5ad53d66384094 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 7 Jul 2019 00:39:52 -0400 Subject: make MongoDB database name configurable (#651) --- docs/technical/web.md | 1 + src/SMAPI.Web/Framework/ConfigModels/MongoDbConfig.cs | 8 +++++--- src/SMAPI.Web/Startup.cs | 4 ++-- src/SMAPI.Web/appsettings.Development.json | 3 ++- src/SMAPI.Web/appsettings.json | 3 ++- 5 files changed, 12 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/docs/technical/web.md b/docs/technical/web.md index db54a87a..f1cb755b 100644 --- a/docs/technical/web.md +++ b/docs/technical/web.md @@ -123,6 +123,7 @@ Initial setup: `MongoDB:Host` | The hostname for the MongoDB instance. `MongoDB:Username` | The login username for the MongoDB instance. `MongoDB:Password` | The login password for the MongoDB instance. + `MongoDB:Database` | The database name (e.g. `smapi` in production or `smapi-edge` in testing environments). To deploy updates: 1. Deploy the web project using [AWS Toolkit for Visual Studio](https://aws.amazon.com/visualstudio/). diff --git a/src/SMAPI.Web/Framework/ConfigModels/MongoDbConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/MongoDbConfig.cs index 352eb960..3c508300 100644 --- a/src/SMAPI.Web/Framework/ConfigModels/MongoDbConfig.cs +++ b/src/SMAPI.Web/Framework/ConfigModels/MongoDbConfig.cs @@ -17,20 +17,22 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels /// The MongoDB password (if any). public string Password { get; set; } + /// The database name. + public string Database { get; set; } + /********* ** Public method *********/ /// Get the MongoDB connection string. - /// The initial database for which to authenticate. - public string GetConnectionString(string authDatabase) + public string GetConnectionString() { bool isLocal = this.Host == "localhost"; bool hasLogin = !string.IsNullOrWhiteSpace(this.Username) && !string.IsNullOrWhiteSpace(this.Password); return $"mongodb{(isLocal ? "" : "+srv")}://" + (hasLogin ? $"{Uri.EscapeDataString(this.Username)}:{Uri.EscapeDataString(this.Password)}@" : "") - + $"{this.Host}/{authDatabase}retryWrites=true&w=majority"; + + $"{this.Host}/{this.Database}?retryWrites=true&w=majority"; } } } diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index 0a8d23a8..315f5f88 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -115,9 +115,9 @@ namespace StardewModdingAPI.Web // init MongoDB { MongoDbConfig mongoConfig = this.Configuration.GetSection("MongoDB").Get(); - string connectionString = mongoConfig.GetConnectionString("smapi"); + string connectionString = mongoConfig.GetConnectionString(); - services.AddSingleton(serv => new MongoClient(connectionString).GetDatabase("smapi")); + services.AddSingleton(serv => new MongoClient(connectionString).GetDatabase(mongoConfig.Database)); services.AddSingleton(serv => new WikiCacheRepository(serv.GetService())); } } diff --git a/src/SMAPI.Web/appsettings.Development.json b/src/SMAPI.Web/appsettings.Development.json index 9b0ec535..3b7ed8bd 100644 --- a/src/SMAPI.Web/appsettings.Development.json +++ b/src/SMAPI.Web/appsettings.Development.json @@ -36,6 +36,7 @@ "MongoDB": { "Host": "localhost", "Username": null, - "Password": null + "Password": null, + "Database": "smapi-edge" } } diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json index 65ccea75..532ea017 100644 --- a/src/SMAPI.Web/appsettings.json +++ b/src/SMAPI.Web/appsettings.json @@ -50,7 +50,8 @@ "MongoDB": { "Host": null, // see top note "Username": null, // see top note - "Password": null // see top note + "Password": null, // see top note + "Database": null // see top note }, "ModCompatibilityList": { -- cgit From a731f5ea9a75aad682c261d747a5d9c71b3d2773 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 7 Jul 2019 00:55:10 -0400 Subject: use better index type (#651) --- docs/technical/web.md | 3 +-- src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheRepository.cs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/docs/technical/web.md b/docs/technical/web.md index f1cb755b..50799e00 100644 --- a/docs/technical/web.md +++ b/docs/technical/web.md @@ -127,5 +127,4 @@ Initial setup: To deploy updates: 1. Deploy the web project using [AWS Toolkit for Visual Studio](https://aws.amazon.com/visualstudio/). -2. If the MongoDB schema changed, delete the collections in the MongoDB database. (They'll be - recreated automatically.) +2. If the MongoDB schema changed, delete the MongoDB database. (It'll be recreated automatically.) diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheRepository.cs index 5b907462..1ae9d38f 100644 --- a/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheRepository.cs +++ b/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheRepository.cs @@ -32,7 +32,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki this.WikiMods = database.GetCollection("wiki-mods"); // add indexes if needed - this.WikiMods.Indexes.CreateOne(new CreateIndexModel(Builders.IndexKeys.Text(p => p.ID))); + this.WikiMods.Indexes.CreateOne(new CreateIndexModel(Builders.IndexKeys.Ascending(p => p.ID))); } /// Get the cached wiki metadata. -- cgit From 20c81f04e0c4db88f550bc1427eb0904697baff0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 8 Jul 2019 17:19:38 -0400 Subject: update compatibility list (#638) --- src/SMAPI.Web/wwwroot/SMAPI.metadata.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI.Web/wwwroot/SMAPI.metadata.json b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json index bde6102c..e4c0a6f6 100644 --- a/src/SMAPI.Web/wwwroot/SMAPI.metadata.json +++ b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json @@ -249,7 +249,7 @@ "Yet Another Harvest With Scythe Mod": { "ID": "bcmpinc.HarvestWithScythe", - "~1.0 | Status": "AssumeBroken" // runtime Harmony error + "~1.1 | Status": "AssumeBroken" // runtime Harmony error }, /********* @@ -297,6 +297,11 @@ "~2.10.10 | Status": "AssumeBroken" // possibly due to PyTK }, + "Decrafting Mod": { + "ID": "MSCFC.DecraftingMod", + "~1.0 | Status": "AssumeBroken" // NRE in ModEntry + }, + "JoJaBan - Arcade Sokoban": { "ID": "Platonymous.JoJaBan", "~0.4.3 | Status": "AssumeBroken" // possibly due to PyTK -- cgit From c9778b567e3fe47a790bcf1d473a7092052a1ba3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 8 Jul 2019 17:48:20 -0400 Subject: update draw logic (#638) --- src/SMAPI/Framework/SGame.cs | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 7222899a..2f642692 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -977,6 +977,13 @@ namespace StardewModdingAPI.Framework Game1.spriteBatch.Draw(Game1.staminaRect, Game1.lightmap.Bounds, color1); for (int index = 0; index < Game1.currentLightSources.Count; ++index) { + LightSource lightSource = Game1.currentLightSources.ElementAt(index); + if (lightSource.PlayerID != 0L && lightSource.PlayerID != Game1.player.UniqueMultiplayerID) + { + Farmer farmerMaybeOffline = Game1.getFarmerMaybeOffline(lightSource.PlayerID); + if (farmerMaybeOffline == null || farmerMaybeOffline.currentLocation != null && farmerMaybeOffline.currentLocation.Name != Game1.currentLocation.Name || (bool)((NetFieldBase)farmerMaybeOffline.hidden)) + continue; + } if (Utility.isOnScreen((Vector2)((NetFieldBase)Game1.currentLightSources.ElementAt(index).position), (int)((double)(float)((NetFieldBase)Game1.currentLightSources.ElementAt(index).radius) * 64.0 * 4.0))) { SpriteBatch spriteBatch = Game1.spriteBatch; @@ -1045,7 +1052,7 @@ namespace StardewModdingAPI.Framework } foreach (Farmer farmerShadow in this._farmerShadows) { - if (!(bool)((NetFieldBase)farmerShadow.swimming) && !farmerShadow.isRidingHorse() && (Game1.currentLocation == null || !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(farmerShadow.getTileLocation()))) + if (!Game1.multiplayer.isDisconnecting(farmerShadow.UniqueMultiplayerID) && !(bool)((NetFieldBase)farmerShadow.swimming) && !farmerShadow.isRidingHorse() && (Game1.currentLocation == null || !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(farmerShadow.getTileLocation()))) { SpriteBatch spriteBatch = Game1.spriteBatch; Texture2D shadowTexture = Game1.shadowTexture; @@ -1283,6 +1290,27 @@ namespace StardewModdingAPI.Framework } if (Game1.currentBillboard != 0) this.drawBillboard(); + if (!Game1.eventUp && Game1.farmEvent == null && (Game1.currentBillboard == 0 && Game1.gameMode == (byte)3) && Game1.isOutdoorMapSmallerThanViewport()) + { + SpriteBatch spriteBatch1 = Game1.spriteBatch; + Texture2D fadeToBlackRect1 = Game1.fadeToBlackRect; + int width1 = -Math.Min(Game1.viewport.X, 4096); + viewport = Game1.graphics.GraphicsDevice.Viewport; + int height1 = viewport.Height; + Microsoft.Xna.Framework.Rectangle destinationRectangle1 = new Microsoft.Xna.Framework.Rectangle(0, 0, width1, height1); + Color black1 = Color.Black; + spriteBatch1.Draw(fadeToBlackRect1, destinationRectangle1, black1); + SpriteBatch spriteBatch2 = Game1.spriteBatch; + Texture2D fadeToBlackRect2 = Game1.fadeToBlackRect; + int x = -Game1.viewport.X + Game1.currentLocation.map.Layers[0].LayerWidth * 64; + viewport = Game1.graphics.GraphicsDevice.Viewport; + int width2 = Math.Min(4096, viewport.Width - (-Game1.viewport.X + Game1.currentLocation.map.Layers[0].LayerWidth * 64)); + viewport = Game1.graphics.GraphicsDevice.Viewport; + int height2 = viewport.Height; + Microsoft.Xna.Framework.Rectangle destinationRectangle2 = new Microsoft.Xna.Framework.Rectangle(x, 0, width2, height2); + Color black2 = Color.Black; + spriteBatch2.Draw(fadeToBlackRect2, destinationRectangle2, black2); + } if ((Game1.displayHUD || Game1.eventUp) && (Game1.currentBillboard == 0 && Game1.gameMode == (byte)3) && (!Game1.freezeControls && !Game1.panMode && !Game1.HostPaused)) { events.RenderingHud.RaiseEmpty(); -- cgit From c6d4381142ef4e871a7b203c4c940a792bcd1a1e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 8 Jul 2019 20:59:55 -0400 Subject: update for SDV 1.4 build change on Linux/Mac (#638) --- src/SMAPI.ModBuildConfig/build/smapi.targets | 12 +++---- src/SMAPI.ModBuildConfig/package.nuspec | 2 +- .../SMAPI.Mods.ConsoleCommands.csproj | 12 +++---- src/SMAPI/SMAPI.csproj | 42 +++++++++------------- 4 files changed, 26 insertions(+), 42 deletions(-) (limited to 'src') diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets index 5eeea91e..78d3a3d4 100644 --- a/src/SMAPI.ModBuildConfig/build/smapi.targets +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -46,6 +46,10 @@ $(GamePath)\$(GameExecutableName).exe $(CopyModReferencesToBuildOutput) + + $(GamePath)\StardewValley.GameData.dll + $(CopyModReferencesToBuildOutput) + $(GamePath)\StardewModdingAPI.exe $(CopyModReferencesToBuildOutput) @@ -82,10 +86,6 @@ $(GamePath)\Netcode.dll $(CopyModReferencesToBuildOutput) - - $(GamePath)\StardewValley.GameData.dll - $(CopyModReferencesToBuildOutput) - @@ -94,10 +94,6 @@ $(GamePath)\MonoGame.Framework.dll $(CopyModReferencesToBuildOutput) - - $(GamePath)\StardewValley.GameData.MonoGame.dll - $(CopyModReferencesToBuildOutput) - diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec index ffa47fdb..76818c9f 100644 --- a/src/SMAPI.ModBuildConfig/package.nuspec +++ b/src/SMAPI.ModBuildConfig/package.nuspec @@ -2,7 +2,7 @@ Pathoschild.Stardew.ModBuildConfig - 3.0.0-beta.4 + 3.0.0-beta.6 Build package for SMAPI mods Pathoschild Pathoschild diff --git a/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj b/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj index 6cceb22d..80e1986e 100644 --- a/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj +++ b/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj @@ -22,16 +22,16 @@ $(GamePath)\$(GameExecutableName).exe False + + $(GamePath)\StardewValley.GameData.dll + False + - - $(GamePath)\StardewValley.GameData.dll - False - $(GamePath)\Netcode.dll False @@ -54,10 +54,6 @@ - - $(GamePath)\StardewValley.GameData.MonoGame.dll - False - $(GamePath)\MonoGame.Framework.dll False diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj index 8b7d6277..994a8715 100644 --- a/src/SMAPI/SMAPI.csproj +++ b/src/SMAPI/SMAPI.csproj @@ -27,23 +27,34 @@ $(GamePath)\$(GameExecutableName).exe False + + $(GamePath)\StardewValley.GameData.dll + False + True True - + + $(GamePath)\GalaxyCSharp.dll + False + + + $(GamePath)\Lidgren.Network.dll + False + + + $(GamePath)\xTile.dll + False + - - $(GamePath)\StardewValley.GameData.dll - False - $(GamePath)\Netcode.dll False @@ -60,16 +71,13 @@ False + - - $(GamePath)\StardewValley.GameData.MonoGame.dll - False - $(GamePath)\MonoGame.Framework.dll False @@ -78,22 +86,6 @@ - - - - $(GamePath)\GalaxyCSharp.dll - False - - - $(GamePath)\Lidgren.Network.dll - False - - - $(GamePath)\xTile.dll - False - - - -- cgit From e2f545484e1a63d55154e2b6a924dfb6d94f7a5a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 12 Jul 2019 20:51:46 -0400 Subject: add asset propagation for critter textures (#652) --- docs/release-notes.md | 3 ++- src/SMAPI/Metadata/CoreAssetPropagator.cs | 34 ++++++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 3f955e78..49bc2eab 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -25,11 +25,12 @@ These changes have not been released yet. * Fixed outdoor tilesheets being seasonalised when added to an indoor location. * For modders: + * Mods are now loaded much earlier in the game launch. This lets mods intercept any content asset, but the game is not fully initialised when `Entry` is called (use the `GameLaunched` event if you need to run code when the game is initialised). * Added support for content pack translations. * Added `IContentPack.HasFile` method. * Added `Context.IsGameLaunched` field. - * Mods are now loaded much earlier in the game launch. This lets mods intercept any content asset, but the game is not fully initialised when `Entry` is called (use the `GameLaunched` event if you need to run code when the game is initialised). * Added separate `LogNetworkTraffic` option to make verbose logging less overwhelmingly verbose. + * Added asset propagation for critter textures. * The installer now recognises custom game paths stored in `stardewvalley.targets`, if any. * When a mod is incompatible, the trace logs now list all detected issues instead of the first one. * Removed all deprecated APIs. diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 9e84c67f..b000d503 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -316,8 +316,12 @@ namespace StardewModdingAPI.Metadata return true; /**** - ** Content\Critters + ** Content\TileSheets ****/ + case "tilesheets\\critters": // Critter constructor + this.ReloadCritterTextures(content, key); + return true; + case "tilesheets\\crops": // Game1.LoadContent Game1.cropSpriteSheet = content.Load(key); return true; @@ -562,6 +566,34 @@ namespace StardewModdingAPI.Metadata return false; } + /// Reload critter textures. + /// The content manager through which to reload the asset. + /// The asset key to reload. + /// Returns the number of reloaded assets. + private int ReloadCritterTextures(LocalizedContentManager content, string key) + { + // get critters + Critter[] critters = + ( + from location in this.GetLocations() + let locCritters = this.Reflection.GetField>(location, "critters").GetValue() + where locCritters != null + from Critter critter in locCritters + where this.GetNormalisedPath(critter.sprite.textureName) == key + select critter + ) + .ToArray(); + if (!critters.Any()) + return 0; + + // update sprites + Texture2D texture = content.Load(key); + foreach (var entry in critters) + this.SetSpriteTexture(entry.sprite, texture); + + return critters.Length; + } + /// Reload the sprites for a fence type. /// The asset key to reload. /// Returns whether any textures were reloaded. -- cgit From 4f7d861ce4767259a556607a167481e074ec4f4c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 13 Jul 2019 22:45:48 -0400 Subject: make SemanticVersion.TryParse public --- docs/release-notes.md | 3 +-- src/SMAPI/SemanticVersion.cs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 49bc2eab..9980c310 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -27,8 +27,7 @@ These changes have not been released yet. * For modders: * Mods are now loaded much earlier in the game launch. This lets mods intercept any content asset, but the game is not fully initialised when `Entry` is called (use the `GameLaunched` event if you need to run code when the game is initialised). * Added support for content pack translations. - * Added `IContentPack.HasFile` method. - * Added `Context.IsGameLaunched` field. + * Added fields and methods: `IContentPack.HasFile`, `Context.IsGameLaunched`, and `SemanticVersion.TryParse`. * Added separate `LogNetworkTraffic` option to make verbose logging less overwhelmingly verbose. * Added asset propagation for critter textures. * The installer now recognises custom game paths stored in `stardewvalley.targets`, if any. diff --git a/src/SMAPI/SemanticVersion.cs b/src/SMAPI/SemanticVersion.cs index acdc92f8..b346cfdd 100644 --- a/src/SMAPI/SemanticVersion.cs +++ b/src/SMAPI/SemanticVersion.cs @@ -141,7 +141,7 @@ namespace StardewModdingAPI /// The version string. /// The parsed representation. /// Returns whether parsing the version succeeded. - internal static bool TryParse(string version, out ISemanticVersion parsed) + public static bool TryParse(string version, out ISemanticVersion parsed) { if (Toolkit.SemanticVersion.TryParse(version, out ISemanticVersion versionImpl)) { -- cgit From 1053232c2039a2815baf2cfa99fe8c554d1350a9 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 14 Jul 2019 14:55:31 -0400 Subject: add asset propagation for DayTimeMoneyBox buttons --- docs/release-notes.md | 2 +- src/SMAPI/Metadata/CoreAssetPropagator.cs | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 9980c310..404424e7 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -29,7 +29,7 @@ These changes have not been released yet. * Added support for content pack translations. * Added fields and methods: `IContentPack.HasFile`, `Context.IsGameLaunched`, and `SemanticVersion.TryParse`. * Added separate `LogNetworkTraffic` option to make verbose logging less overwhelmingly verbose. - * Added asset propagation for critter textures. + * Added asset propagation for critter textures and `DayTimeMoneyBox` buttons. * The installer now recognises custom game paths stored in `stardewvalley.targets`, if any. * When a mod is incompatible, the trace logs now list all detected issues instead of the first one. * Removed all deprecated APIs. diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index b000d503..3fbca04a 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -293,6 +293,11 @@ namespace StardewModdingAPI.Metadata case "loosesprites\\cursors": // Game1.LoadContent Game1.mouseCursors = content.Load(key); + foreach (DayTimeMoneyBox menu in Game1.onScreenMenus.OfType()) + { + foreach (ClickableTextureComponent button in new[] { menu.questButton, menu.zoomInButton, menu.zoomOutButton }) + button.texture = Game1.mouseCursors; + } return true; case "loosesprites\\daybg": // Game1.LoadContent -- cgit From 48f211f5447ec7580b9f9bba63eab0ec6b1ec5c7 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 14 Jul 2019 17:49:33 -0400 Subject: add metadata links to mod compatibility list --- docs/release-notes.md | 4 ++++ src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs | 10 ++++++++++ src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs | 6 ++++++ src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs | 5 +++++ src/SMAPI.Web/ViewModels/ModModel.cs | 5 +++++ src/SMAPI.Web/Views/Mods/Index.cshtml | 9 ++++++++- 6 files changed, 38 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 404424e7..c07ae750 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -24,6 +24,10 @@ These changes have not been released yet. * Fixed map reloads not updating door warps. * Fixed outdoor tilesheets being seasonalised when added to an indoor location. +* For the mod compatibility list: + * Clicking a mod link now automatically adds it to the visible mods when the list is filtered. + * Added metadata links to advanced info, if any. + * For modders: * Mods are now loaded much earlier in the game launch. This lets mods intercept any content asset, but the game is not fully initialised when `Entry` is called (use the `GameLaunched` event if you need to run code when the game is initialised). * Added support for content pack translations. diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs index 3e9b8ea6..24aea5e8 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs @@ -127,6 +127,15 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki } } + // parse links + List> metadataLinks = new List>(); + foreach (HtmlNode linkElement in node.Descendants("td").Last().Descendants("a").Skip(1)) // skip anchor link + { + string text = linkElement.InnerText.Trim(); + Uri url = new Uri(linkElement.GetAttributeValue("href", "")); + metadataLinks.Add(Tuple.Create(url, text)); + } + // yield model yield return new WikiModEntry { @@ -143,6 +152,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki Compatibility = compatibility, BetaCompatibility = betaCompatibility, Warnings = warnings, + MetadataLinks = metadataLinks.ToArray(), Anchor = anchor }; } diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs index cf416cc6..556796c5 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs @@ -1,3 +1,6 @@ +using System; +using System.Collections.Generic; + namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki { /// A mod entry in the wiki list. @@ -48,6 +51,9 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki /// The human-readable warnings for players about this mod. public string[] Warnings { get; set; } + /// Extra metadata links (usually for open pull requests). + public Tuple[] MetadataLinks { get; set; } + /// The link anchor for the mod entry in the wiki compatibility list. public string Anchor { get; set; } } diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs b/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs index f4331f8d..850272eb 100644 --- a/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs +++ b/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs @@ -58,6 +58,9 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki /// The human-readable warnings for players about this mod. public string[] Warnings { get; set; } + /// Extra metadata links (usually for open pull requests). + public Tuple[] MetadataLinks { get; set; } + /// The link anchor for the mod entry in the wiki compatibility list. public string Anchor { get; set; } @@ -122,6 +125,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki this.CustomSourceUrl = mod.CustomSourceUrl; this.CustomUrl = mod.CustomUrl; this.ContentPackFor = mod.ContentPackFor; + this.MetadataLinks = mod.MetadataLinks; this.Warnings = mod.Warnings; this.Anchor = mod.Anchor; @@ -156,6 +160,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki CustomUrl = this.CustomUrl, ContentPackFor = this.ContentPackFor, Warnings = this.Warnings, + MetadataLinks = this.MetadataLinks, Anchor = this.Anchor, // stable compatibility diff --git a/src/SMAPI.Web/ViewModels/ModModel.cs b/src/SMAPI.Web/ViewModels/ModModel.cs index 8668f67b..b57e03f8 100644 --- a/src/SMAPI.Web/ViewModels/ModModel.cs +++ b/src/SMAPI.Web/ViewModels/ModModel.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; @@ -37,6 +38,9 @@ namespace StardewModdingAPI.Web.ViewModels /// The human-readable warnings for players about this mod. public string[] Warnings { get; set; } + /// Extra metadata links (usually for open pull requests). + public Tuple[] MetadataLinks { get; set; } + /// A unique identifier for the mod that can be used in an anchor URL. public string Slug { get; set; } @@ -61,6 +65,7 @@ namespace StardewModdingAPI.Web.ViewModels this.BetaCompatibility = entry.BetaCompatibility != null ? new ModCompatibilityModel(entry.BetaCompatibility) : null; this.ModPages = this.GetModPageUrls(entry).ToArray(); this.Warnings = entry.Warnings; + this.MetadataLinks = entry.MetadataLinks; this.Slug = entry.Anchor; } diff --git a/src/SMAPI.Web/Views/Mods/Index.cshtml b/src/SMAPI.Web/Views/Mods/Index.cshtml index 8293fbe2..2bc135c3 100644 --- a/src/SMAPI.Web/Views/Mods/Index.cshtml +++ b/src/SMAPI.Web/Views/Mods/Index.cshtml @@ -92,7 +92,14 @@ no source - # + + # + + + + -- cgit From 1bf399ec23368a0666203298768a4b24b63d6a88 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 14 Jul 2019 22:27:00 -0400 Subject: add dev note field to compatibility list --- docs/release-notes.md | 2 +- src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs | 2 ++ src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs | 3 +++ src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs | 5 +++++ src/SMAPI.Web/ViewModels/ModModel.cs | 4 ++++ src/SMAPI.Web/Views/Mods/Index.cshtml | 2 ++ 6 files changed, 17 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index c07ae750..bc5e5a89 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -26,7 +26,7 @@ These changes have not been released yet. * For the mod compatibility list: * Clicking a mod link now automatically adds it to the visible mods when the list is filtered. - * Added metadata links to advanced info, if any. + * Added metadata links and dev notes (if any) to advanced info. * For modders: * Mods are now loaded much earlier in the game launch. This lets mods intercept any content asset, but the game is not fully initialised when `Entry` is called (use the `GameLaunched` event if you need to run code when the game is initialised). diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs index 24aea5e8..ab6a2517 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs @@ -99,6 +99,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki string customUrl = this.GetAttribute(node, "data-url"); string anchor = this.GetAttribute(node, "id"); string contentPackFor = this.GetAttribute(node, "data-content-pack-for"); + string devNote = this.GetAttribute(node, "data-dev-note"); // parse stable compatibility WikiCompatibilityInfo compatibility = new WikiCompatibilityInfo @@ -153,6 +154,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki BetaCompatibility = betaCompatibility, Warnings = warnings, MetadataLinks = metadataLinks.ToArray(), + DevNote = devNote, Anchor = anchor }; } diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs index 556796c5..fff86891 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs @@ -54,6 +54,9 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki /// Extra metadata links (usually for open pull requests). public Tuple[] MetadataLinks { get; set; } + /// Special notes intended for developers who maintain unofficial updates or submit pull requests. + public string DevNote { get; set; } + /// The link anchor for the mod entry in the wiki compatibility list. public string Anchor { get; set; } } diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs b/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs index 850272eb..bf1e2be2 100644 --- a/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs +++ b/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs @@ -61,6 +61,9 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki /// Extra metadata links (usually for open pull requests). public Tuple[] MetadataLinks { get; set; } + /// Special notes intended for developers who maintain unofficial updates or submit pull requests. + public string DevNote { get; set; } + /// The link anchor for the mod entry in the wiki compatibility list. public string Anchor { get; set; } @@ -127,6 +130,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki this.ContentPackFor = mod.ContentPackFor; this.MetadataLinks = mod.MetadataLinks; this.Warnings = mod.Warnings; + this.DevNote = mod.DevNote; this.Anchor = mod.Anchor; // stable compatibility @@ -161,6 +165,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki ContentPackFor = this.ContentPackFor, Warnings = this.Warnings, MetadataLinks = this.MetadataLinks, + DevNote = this.DevNote, Anchor = this.Anchor, // stable compatibility diff --git a/src/SMAPI.Web/ViewModels/ModModel.cs b/src/SMAPI.Web/ViewModels/ModModel.cs index b57e03f8..5a343640 100644 --- a/src/SMAPI.Web/ViewModels/ModModel.cs +++ b/src/SMAPI.Web/ViewModels/ModModel.cs @@ -41,6 +41,9 @@ namespace StardewModdingAPI.Web.ViewModels /// Extra metadata links (usually for open pull requests). public Tuple[] MetadataLinks { get; set; } + /// Special notes intended for developers who maintain unofficial updates or submit pull requests. + public string DevNote { get; set; } + /// A unique identifier for the mod that can be used in an anchor URL. public string Slug { get; set; } @@ -66,6 +69,7 @@ namespace StardewModdingAPI.Web.ViewModels this.ModPages = this.GetModPageUrls(entry).ToArray(); this.Warnings = entry.Warnings; this.MetadataLinks = entry.MetadataLinks; + this.DevNote = entry.DevNote; this.Slug = entry.Anchor; } diff --git a/src/SMAPI.Web/Views/Mods/Index.cshtml b/src/SMAPI.Web/Views/Mods/Index.cshtml index 2bc135c3..2d45a64d 100644 --- a/src/SMAPI.Web/Views/Mods/Index.cshtml +++ b/src/SMAPI.Web/Views/Mods/Index.cshtml @@ -98,6 +98,8 @@ + + [dev note] -- cgit From b2134035b78c06362a4b973502e679be0b5a0de3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 17 Jul 2019 13:07:59 -0400 Subject: update NuGet packages --- docs/release-notes.md | 2 +- .../SMAPI.ModBuildConfig.Analyzer.Tests.csproj | 6 +++--- src/SMAPI.Tests/SMAPI.Tests.csproj | 10 +++------- src/SMAPI.Toolkit/SMAPI.Toolkit.csproj | 6 +++--- src/SMAPI.Web/SMAPI.Web.csproj | 16 ++++++++-------- src/SMAPI/SMAPI.csproj | 2 +- 6 files changed, 19 insertions(+), 23 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index bc5e5a89..e22d3718 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -38,7 +38,7 @@ These changes have not been released yet. * When a mod is incompatible, the trace logs now list all detected issues instead of the first one. * Removed all deprecated APIs. * Removed the `Monitor.ExitGameImmediately` method. - * Updated to Json.NET 12.0.1. + * Updated to Json.NET 12.0.2. * Fixed issue where mod changes weren't tracked correctly for raising events in some cases. Events now reflect a frozen snapshot of the game state, and any mod changes are reflected in the next event tick. * Fixed `LoadStageChanged` event not raising correct flags in some cases when creating a new save. 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 44e7ffab..8edd5b1e 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj +++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj @@ -7,9 +7,9 @@ - - - + + + diff --git a/src/SMAPI.Tests/SMAPI.Tests.csproj b/src/SMAPI.Tests/SMAPI.Tests.csproj index 2f632a49..41e12d7d 100644 --- a/src/SMAPI.Tests/SMAPI.Tests.csproj +++ b/src/SMAPI.Tests/SMAPI.Tests.csproj @@ -16,13 +16,9 @@ - - - - - - - + + + diff --git a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj index 53bbe1ac..ad7ecd20 100644 --- a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj +++ b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj @@ -15,9 +15,9 @@ - - - + + + diff --git a/src/SMAPI.Web/SMAPI.Web.csproj b/src/SMAPI.Web/SMAPI.Web.csproj index 90641611..2f90389b 100644 --- a/src/SMAPI.Web/SMAPI.Web.csproj +++ b/src/SMAPI.Web/SMAPI.Web.csproj @@ -16,15 +16,15 @@ - - - - - - - + + + + + + + - + diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj index 994a8715..d518b158 100644 --- a/src/SMAPI/SMAPI.csproj +++ b/src/SMAPI/SMAPI.csproj @@ -19,7 +19,7 @@ - + -- cgit From 79622d79b8b0ef6850b9431392dc57819cb89346 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 17 Jul 2019 13:15:48 -0400 Subject: Update Mono.Cecil package --- docs/release-notes.md | 2 +- src/SMAPI/SMAPI.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index e22d3718..ea988fe5 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -38,7 +38,7 @@ These changes have not been released yet. * When a mod is incompatible, the trace logs now list all detected issues instead of the first one. * Removed all deprecated APIs. * Removed the `Monitor.ExitGameImmediately` method. - * Updated to Json.NET 12.0.2. + * Updated dependencies (including Json.NET 11.0.2 → 12.0.2, Mono.Cecil 0.10.1 → 0.10.4). * Fixed issue where mod changes weren't tracked correctly for raising events in some cases. Events now reflect a frozen snapshot of the game state, and any mod changes are reflected in the next event tick. * Fixed `LoadStageChanged` event not raising correct flags in some cases when creating a new save. diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj index d518b158..0157e66a 100644 --- a/src/SMAPI/SMAPI.csproj +++ b/src/SMAPI/SMAPI.csproj @@ -18,7 +18,7 @@ - + -- cgit From db141f397631901353ee7b0600b71cc74672d9e8 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 17 Jul 2019 13:25:15 -0400 Subject: update code analysis package This commit updates the code analysis package to the one available in Visual Studio 2017 v15.9; the next version matches Visual Studio 2019. --- .../SMAPI.ModBuildConfig.Analyzer.Tests.csproj | 2 +- src/SMAPI.ModBuildConfig.Analyzer/SMAPI.ModBuildConfig.Analyzer.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'src') 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 8edd5b1e..40ceba3c 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj +++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj @@ -6,7 +6,7 @@ - + diff --git a/src/SMAPI.ModBuildConfig.Analyzer/SMAPI.ModBuildConfig.Analyzer.csproj b/src/SMAPI.ModBuildConfig.Analyzer/SMAPI.ModBuildConfig.Analyzer.csproj index 1d8d7227..f2b0a912 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer/SMAPI.ModBuildConfig.Analyzer.csproj +++ b/src/SMAPI.ModBuildConfig.Analyzer/SMAPI.ModBuildConfig.Analyzer.csproj @@ -13,7 +13,7 @@ - + -- cgit From 8700084300e7bea240693312b6e99d1c9482d57d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 17 Jul 2019 13:47:14 -0400 Subject: bump .NET Standard version used by the mod build package to match toolkit --- build/prepare-nuget-package.targets | 2 +- src/SMAPI.ModBuildConfig.Analyzer/SMAPI.ModBuildConfig.Analyzer.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/build/prepare-nuget-package.targets b/build/prepare-nuget-package.targets index 9c1d9c2a..4cadae3c 100644 --- a/build/prepare-nuget-package.targets +++ b/build/prepare-nuget-package.targets @@ -17,6 +17,6 @@ - +
diff --git a/src/SMAPI.ModBuildConfig.Analyzer/SMAPI.ModBuildConfig.Analyzer.csproj b/src/SMAPI.ModBuildConfig.Analyzer/SMAPI.ModBuildConfig.Analyzer.csproj index f2b0a912..d812e9c6 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer/SMAPI.ModBuildConfig.Analyzer.csproj +++ b/src/SMAPI.ModBuildConfig.Analyzer/SMAPI.ModBuildConfig.Analyzer.csproj @@ -1,7 +1,7 @@  - netstandard1.3 + netstandard2.0 false false bin -- cgit From be1f09f5f9d3318a4bbdf593f671f4eacdd4395d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 18 Jul 2019 15:13:29 -0400 Subject: update obsolete code --- .../Framework/Clients/Pastebin/PastebinClient.cs | 32 ++++++---------------- src/SMAPI.Web/Startup.cs | 8 ++---- 2 files changed, 10 insertions(+), 30 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Web/Framework/Clients/Pastebin/PastebinClient.cs b/src/SMAPI.Web/Framework/Clients/Pastebin/PastebinClient.cs index 12c3e83f..1e46f2dc 100644 --- a/src/SMAPI.Web/Framework/Clients/Pastebin/PastebinClient.cs +++ b/src/SMAPI.Web/Framework/Clients/Pastebin/PastebinClient.cs @@ -1,11 +1,8 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; -using System.Text; using System.Threading.Tasks; -using System.Web; using Pathoschild.Http.Client; namespace StardewModdingAPI.Web.Framework.Clients.Pastebin @@ -82,15 +79,15 @@ namespace StardewModdingAPI.Web.Framework.Clients.Pastebin // post to API string response = await this.Client .PostAsync("api/api_post.php") - .WithBodyContent(this.GetFormUrlEncodedContent(new Dictionary + .WithBody(p => p.FormUrlEncoded(new { - ["api_option"] = "paste", - ["api_user_key"] = this.UserKey, - ["api_dev_key"] = this.DevKey, - ["api_paste_private"] = "1", // unlisted - ["api_paste_name"] = $"SMAPI log {DateTime.UtcNow:s}", - ["api_paste_expire_date"] = "N", // never expire - ["api_paste_code"] = content + api_option = "paste", + api_user_key = this.UserKey, + api_dev_key = this.DevKey, + api_paste_private = 1, // unlisted + api_paste_name = $"SMAPI log {DateTime.UtcNow:s}", + api_paste_expire_date = "N", // never expire + api_paste_code = content })) .AsString(); @@ -117,18 +114,5 @@ namespace StardewModdingAPI.Web.Framework.Clients.Pastebin { this.Client.Dispose(); } - - - /********* - ** Private methods - *********/ - /// Build an HTTP content body with form-url-encoded content. - /// The content to encode. - /// This bypasses an issue where restricts the body length to the maximum size of a URL, which isn't applicable here. - private HttpContent GetFormUrlEncodedContent(IDictionary data) - { - string body = string.Join("&", from arg in data select $"{HttpUtility.UrlEncode(arg.Key)}={HttpUtility.UrlEncode(arg.Value)}"); - return new StringContent(body, Encoding.UTF8, "application/x-www-form-urlencoded"); - } } } diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index 315f5f88..7beb0bcc 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -5,7 +5,6 @@ using Microsoft.AspNetCore.Rewrite; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using MongoDB.Driver; using Newtonsoft.Json; using StardewModdingAPI.Toolkit.Serialisation; @@ -57,6 +56,7 @@ namespace StardewModdingAPI.Web .Configure(this.Configuration.GetSection("Site")) .Configure(this.Configuration.GetSection("MongoDB")) .Configure(options => options.ConstraintMap.Add("semanticVersion", typeof(VersionConstraint))) + .AddLogging() .AddMemoryCache() .AddMvc() .ConfigureApplicationPartManager(manager => manager.FeatureProviders.Add(new InternalControllerFeatureProvider())) @@ -125,12 +125,8 @@ namespace StardewModdingAPI.Web /// The method called by the runtime to configure the HTTP request pipeline. /// The application builder. /// The hosting environment. - /// The logger factory. - public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + public void Configure(IApplicationBuilder app, IHostingEnvironment env) { - loggerFactory.AddConsole(this.Configuration.GetSection("Logging")); - loggerFactory.AddDebug(); - if (env.IsDevelopment()) app.UseDeveloperExceptionPage(); -- cgit From 88110dffbf4bace738e8f29133bfb3ac4b265e2c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 18 Jul 2019 17:17:39 -0400 Subject: reduce log level in development --- src/SMAPI.Web/appsettings.Development.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Web/appsettings.Development.json b/src/SMAPI.Web/appsettings.Development.json index 3b7ed8bd..c50557b5 100644 --- a/src/SMAPI.Web/appsettings.Development.json +++ b/src/SMAPI.Web/appsettings.Development.json @@ -11,9 +11,7 @@ "Logging": { "IncludeScopes": false, "LogLevel": { - "Default": "Debug", - "System": "Information", - "Microsoft": "Information" + "Default": "Warning" } }, -- cgit From ce6cedaf4be53d52f2e558055b91e515b92e4c83 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 19 Jul 2019 13:15:45 -0400 Subject: add background fetch for mod compatibility list (#651) --- docs/release-notes.md | 2 + src/SMAPI.Web/BackgroundService.cs | 92 +++++++++++ src/SMAPI.Web/Controllers/ModsController.cs | 33 ++-- .../Framework/Caching/BaseCacheRepository.cs | 6 +- .../Framework/Caching/Wiki/CachedWikiMetadata.cs | 2 +- .../Framework/Caching/Wiki/IWikiCacheRepository.cs | 4 +- .../ConfigModels/BackgroundServicesConfig.cs | 12 ++ .../ConfigModels/ModCompatibilityListConfig.cs | 6 +- src/SMAPI.Web/SMAPI.Web.csproj | 2 + src/SMAPI.Web/Startup.cs | 48 ++++-- src/SMAPI.Web/ViewModels/ModListModel.cs | 19 ++- src/SMAPI.Web/Views/Mods/Index.cshtml | 172 +++++++++++---------- src/SMAPI.Web/appsettings.Development.json | 7 - src/SMAPI.Web/appsettings.json | 9 +- src/SMAPI.Web/wwwroot/Content/css/mods.css | 57 ++++--- 15 files changed, 317 insertions(+), 154 deletions(-) create mode 100644 src/SMAPI.Web/BackgroundService.cs create mode 100644 src/SMAPI.Web/Framework/ConfigModels/BackgroundServicesConfig.cs (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index ea988fe5..9c3e3d28 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -25,6 +25,8 @@ These changes have not been released yet. * Fixed outdoor tilesheets being seasonalised when added to an indoor location. * For the mod compatibility list: + * Now loads faster (since data is fetched in a background service). + * Now continues working with cached data when the wiki is offline. * Clicking a mod link now automatically adds it to the visible mods when the list is filtered. * Added metadata links and dev notes (if any) to advanced info. diff --git a/src/SMAPI.Web/BackgroundService.cs b/src/SMAPI.Web/BackgroundService.cs new file mode 100644 index 00000000..2ccfd5f7 --- /dev/null +++ b/src/SMAPI.Web/BackgroundService.cs @@ -0,0 +1,92 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Hangfire; +using Microsoft.Extensions.Hosting; +using StardewModdingAPI.Toolkit; +using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; +using StardewModdingAPI.Web.Framework.Caching.Wiki; + +namespace StardewModdingAPI.Web +{ + /// A hosted service which runs background data updates. + /// Task methods need to be static, since otherwise Hangfire will try to serialise the entire instance. + internal class BackgroundService : IHostedService, IDisposable + { + /********* + ** Fields + *********/ + /// The background task server. + private static BackgroundJobServer JobServer; + + /// The cache in which to store mod metadata. + private static IWikiCacheRepository WikiCache; + + + /********* + ** Public methods + *********/ + /**** + ** Hosted service + ****/ + /// Construct an instance. + /// The cache in which to store mod metadata. + public BackgroundService(IWikiCacheRepository wikiCache) + { + BackgroundService.WikiCache = wikiCache; + } + + /// Start the service. + /// Tracks whether the start process has been aborted. + public Task StartAsync(CancellationToken cancellationToken) + { + this.TryInit(); + + // set startup tasks + BackgroundJob.Enqueue(() => BackgroundService.UpdateWikiAsync()); + + // set recurring tasks + RecurringJob.AddOrUpdate(() => BackgroundService.UpdateWikiAsync(), "*/10 * * * *"); + + return Task.CompletedTask; + } + + /// Triggered when the application host is performing a graceful shutdown. + /// Tracks whether the shutdown process should no longer be graceful. + public async Task StopAsync(CancellationToken cancellationToken) + { + if (BackgroundService.JobServer != null) + await BackgroundService.JobServer.WaitForShutdownAsync(cancellationToken); + } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public void Dispose() + { + BackgroundService.JobServer?.Dispose(); + } + + /**** + ** Tasks + ****/ + /// Update the cached wiki metadata. + public static async Task UpdateWikiAsync() + { + WikiModList wikiCompatList = await new ModToolkit().GetWikiCompatibilityListAsync(); + BackgroundService.WikiCache.SaveWikiData(wikiCompatList.StableVersion, wikiCompatList.BetaVersion, wikiCompatList.Mods, out _, out _); + } + + + /********* + ** Private method + *********/ + /// Initialise the background service if it's not already initialised. + /// The background service is already initialised. + private void TryInit() + { + if (BackgroundService.JobServer != null) + throw new InvalidOperationException("The scheduler service is already started."); + + BackgroundService.JobServer = new BackgroundJobServer(); + } + } +} diff --git a/src/SMAPI.Web/Controllers/ModsController.cs b/src/SMAPI.Web/Controllers/ModsController.cs index b6040e06..b621ded0 100644 --- a/src/SMAPI.Web/Controllers/ModsController.cs +++ b/src/SMAPI.Web/Controllers/ModsController.cs @@ -1,9 +1,7 @@ using System.Linq; using System.Text.RegularExpressions; -using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; -using StardewModdingAPI.Toolkit; using StardewModdingAPI.Web.Framework.Caching.Wiki; using StardewModdingAPI.Web.Framework.ConfigModels; using StardewModdingAPI.Web.ViewModels; @@ -19,8 +17,8 @@ namespace StardewModdingAPI.Web.Controllers /// The cache in which to store mod metadata. private readonly IWikiCacheRepository Cache; - /// The number of minutes successful update checks should be cached before refetching them. - private readonly int CacheMinutes; + /// The number of minutes before which wiki data should be considered old. + private readonly int StaleMinutes; /********* @@ -34,15 +32,15 @@ namespace StardewModdingAPI.Web.Controllers ModCompatibilityListConfig config = configProvider.Value; this.Cache = cache; - this.CacheMinutes = config.CacheMinutes; + this.StaleMinutes = config.StaleMinutes; } /// Display information for all mods. [HttpGet] [Route("mods")] - public async Task Index() + public ViewResult Index() { - return this.View("Index", await this.FetchDataAsync()); + return this.View("Index", this.FetchData()); } @@ -50,25 +48,22 @@ namespace StardewModdingAPI.Web.Controllers ** Private methods *********/ /// Asynchronously fetch mod metadata from the wiki. - public async Task FetchDataAsync() + public ModListModel FetchData() { - // refresh cache - CachedWikiMod[] mods; - if (!this.Cache.TryGetWikiMetadata(out CachedWikiMetadata metadata) || this.Cache.IsStale(metadata.LastUpdated, this.CacheMinutes)) - { - var wikiCompatList = await new ModToolkit().GetWikiCompatibilityListAsync(); - this.Cache.SaveWikiData(wikiCompatList.StableVersion, wikiCompatList.BetaVersion, wikiCompatList.Mods, out metadata, out mods); - } - else - mods = this.Cache.GetWikiMods().ToArray(); + // fetch cached data + if (!this.Cache.TryGetWikiMetadata(out CachedWikiMetadata metadata)) + return new ModListModel(); // build model return new ModListModel( stableVersion: metadata.StableVersion, betaVersion: metadata.BetaVersion, - mods: mods + mods: this.Cache + .GetWikiMods() .Select(mod => new ModModel(mod.GetModel())) - .OrderBy(p => Regex.Replace(p.Name.ToLower(), "[^a-z0-9]", "")) // ignore case, spaces, and special characters when sorting + .OrderBy(p => Regex.Replace(p.Name.ToLower(), "[^a-z0-9]", "")), // ignore case, spaces, and special characters when sorting + lastUpdated: metadata.LastUpdated, + isStale: this.Cache.IsStale(metadata.LastUpdated, this.StaleMinutes) ); } } diff --git a/src/SMAPI.Web/Framework/Caching/BaseCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/BaseCacheRepository.cs index 904455c5..f5354b93 100644 --- a/src/SMAPI.Web/Framework/Caching/BaseCacheRepository.cs +++ b/src/SMAPI.Web/Framework/Caching/BaseCacheRepository.cs @@ -10,10 +10,10 @@ namespace StardewModdingAPI.Web.Framework.Caching *********/ /// Whether cached data is stale. /// The date when the data was updated. - /// The age in minutes before data is considered stale. - public bool IsStale(DateTimeOffset lastUpdated, int cacheMinutes) + /// The age in minutes before data is considered stale. + public bool IsStale(DateTimeOffset lastUpdated, int staleMinutes) { - return lastUpdated < DateTimeOffset.UtcNow.AddMinutes(-cacheMinutes); + return lastUpdated < DateTimeOffset.UtcNow.AddMinutes(-staleMinutes); } } } diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMetadata.cs b/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMetadata.cs index de1ea9db..4d6b4b10 100644 --- a/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMetadata.cs +++ b/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMetadata.cs @@ -37,7 +37,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki { this.StableVersion = stableVersion; this.BetaVersion = betaVersion; - this.LastUpdated = DateTimeOffset.UtcNow;; + this.LastUpdated = DateTimeOffset.UtcNow; } } } diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs index d319db69..6031123d 100644 --- a/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs +++ b/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs @@ -17,8 +17,8 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki /// Whether cached data is stale. /// The date when the data was updated. - /// The age in minutes before data is considered stale. - bool IsStale(DateTimeOffset lastUpdated, int cacheMinutes); + /// The age in minutes before data is considered stale. + bool IsStale(DateTimeOffset lastUpdated, int staleMinutes); /// Get the cached wiki mods. /// A filter to apply, if any. diff --git a/src/SMAPI.Web/Framework/ConfigModels/BackgroundServicesConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/BackgroundServicesConfig.cs new file mode 100644 index 00000000..de871c9a --- /dev/null +++ b/src/SMAPI.Web/Framework/ConfigModels/BackgroundServicesConfig.cs @@ -0,0 +1,12 @@ +namespace StardewModdingAPI.Web.Framework.ConfigModels +{ + /// The config settings for background services. + internal class BackgroundServicesConfig + { + /********* + ** Accessors + *********/ + /// Whether to enable background update services. + public bool Enabled { get; set; } + } +} diff --git a/src/SMAPI.Web/Framework/ConfigModels/ModCompatibilityListConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ModCompatibilityListConfig.cs index d9ac9f02..24b540cd 100644 --- a/src/SMAPI.Web/Framework/ConfigModels/ModCompatibilityListConfig.cs +++ b/src/SMAPI.Web/Framework/ConfigModels/ModCompatibilityListConfig.cs @@ -1,12 +1,12 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels { - /// The config settings for mod compatibility list. + /// The config settings for the mod compatibility list. internal class ModCompatibilityListConfig { /********* ** Accessors *********/ - /// The number of minutes data from the wiki should be cached before refetching it. - public int CacheMinutes { get; set; } + /// The number of minutes before which wiki data should be considered old. + public int StaleMinutes { get; set; } } } diff --git a/src/SMAPI.Web/SMAPI.Web.csproj b/src/SMAPI.Web/SMAPI.Web.csproj index 2f90389b..d53914d5 100644 --- a/src/SMAPI.Web/SMAPI.Web.csproj +++ b/src/SMAPI.Web/SMAPI.Web.csproj @@ -16,6 +16,8 @@ + + diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index 7beb0bcc..bdfa5ed9 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using Hangfire; +using Hangfire.Mongo; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Rewrite; @@ -49,12 +51,13 @@ namespace StardewModdingAPI.Web /// The service injection container. public void ConfigureServices(IServiceCollection services) { - // init configuration + // init basic services services + .Configure(this.Configuration.GetSection("BackgroundServices")) .Configure(this.Configuration.GetSection("ModCompatibilityList")) .Configure(this.Configuration.GetSection("ModUpdateCheck")) - .Configure(this.Configuration.GetSection("Site")) .Configure(this.Configuration.GetSection("MongoDB")) + .Configure(this.Configuration.GetSection("Site")) .Configure(options => options.ConstraintMap.Add("semanticVersion", typeof(VersionConstraint))) .AddLogging() .AddMemoryCache() @@ -69,6 +72,33 @@ namespace StardewModdingAPI.Web options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; }); + // init background service + { + BackgroundServicesConfig config = this.Configuration.GetSection("BackgroundServices").Get(); + if (config.Enabled) + services.AddHostedService(); + } + + // init MongoDB + MongoDbConfig mongoConfig = this.Configuration.GetSection("MongoDB").Get(); + string mongoConnectionStr = mongoConfig.GetConnectionString(); + services.AddSingleton(serv => new MongoClient(mongoConnectionStr).GetDatabase(mongoConfig.Database)); + services.AddSingleton(serv => new WikiCacheRepository(serv.GetService())); + + // init Hangfire (needs MongoDB) + services + .AddHangfire(config => + { + config + .SetDataCompatibilityLevel(CompatibilityLevel.Version_170) + .UseSimpleAssemblyNameTypeSerializer() + .UseRecommendedSerializerSettings() + .UseMongoStorage(mongoConnectionStr, $"{mongoConfig.Database}-hangfire", new MongoStorageOptions + { + MigrationOptions = new MongoMigrationOptions(MongoMigrationStrategy.Drop) + }); + }); + // init API clients { ApiClientsConfig api = this.Configuration.GetSection("ApiClients").Get(); @@ -111,15 +141,6 @@ namespace StardewModdingAPI.Web devKey: api.PastebinDevKey )); } - - // init MongoDB - { - MongoDbConfig mongoConfig = this.Configuration.GetSection("MongoDB").Get(); - string connectionString = mongoConfig.GetConnectionString(); - - services.AddSingleton(serv => new MongoClient(connectionString).GetDatabase(mongoConfig.Database)); - services.AddSingleton(serv => new WikiCacheRepository(serv.GetService())); - } } /// The method called by the runtime to configure the HTTP request pipeline. @@ -127,9 +148,9 @@ namespace StardewModdingAPI.Web /// The hosting environment. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { + // basic config if (env.IsDevelopment()) app.UseDeveloperExceptionPage(); - app .UseCors(policy => policy .AllowAnyHeader() @@ -140,6 +161,9 @@ namespace StardewModdingAPI.Web .UseRewriter(this.GetRedirectRules()) .UseStaticFiles() // wwwroot folder .UseMvc(); + + // config Hangfire + app.UseHangfireDashboard("/tasks"); } diff --git a/src/SMAPI.Web/ViewModels/ModListModel.cs b/src/SMAPI.Web/ViewModels/ModListModel.cs index 3b87d393..ff7513bc 100644 --- a/src/SMAPI.Web/ViewModels/ModListModel.cs +++ b/src/SMAPI.Web/ViewModels/ModListModel.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; @@ -18,19 +19,35 @@ namespace StardewModdingAPI.Web.ViewModels /// The mods to display. public ModModel[] Mods { get; set; } + /// When the data was last updated. + public DateTimeOffset LastUpdated { get; set; } + + /// Whether the data hasn't been updated in a while. + public bool IsStale { get; set; } + + /// Whether the mod metadata is available. + public bool HasData => this.Mods != null; + /********* ** Public methods *********/ + /// Construct an empty instance. + public ModListModel() { } + /// Construct an instance. /// The current stable version of the game. /// The current beta version of the game (if any). /// The mods to display. - public ModListModel(string stableVersion, string betaVersion, IEnumerable mods) + /// When the data was last updated. + /// Whether the data hasn't been updated in a while. + public ModListModel(string stableVersion, string betaVersion, IEnumerable mods, DateTimeOffset lastUpdated, bool isStale) { this.StableVersion = stableVersion; this.BetaVersion = betaVersion; this.Mods = mods.ToArray(); + this.LastUpdated = lastUpdated; + this.IsStale = isStale; } } } diff --git a/src/SMAPI.Web/Views/Mods/Index.cshtml b/src/SMAPI.Web/Views/Mods/Index.cshtml index 2d45a64d..aa7c0678 100644 --- a/src/SMAPI.Web/Views/Mods/Index.cshtml +++ b/src/SMAPI.Web/Views/Mods/Index.cshtml @@ -18,92 +18,104 @@ } -
-
-

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

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

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

+
+
+

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

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

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

- } -
+

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

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

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

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

Upload a JSON file

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

Validation errors

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

No errors found.

+ } + } + +

Raw content

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

SMAPI

+ +

Tools

+
diff --git a/src/SMAPI.Web/appsettings.Development.json b/src/SMAPI.Web/appsettings.Development.json index e6b4a1b1..baf7efb7 100644 --- a/src/SMAPI.Web/appsettings.Development.json +++ b/src/SMAPI.Web/appsettings.Development.json @@ -12,6 +12,7 @@ "RootUrl": "http://localhost:59482/", "ModListUrl": "http://localhost:59482/mods/", "LogParserUrl": "http://localhost:59482/log/", + "JsonValidatorUrl": "http://localhost:59482/json/", "BetaEnabled": false, "BetaBlurb": null }, diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json index f9777f87..a440cf42 100644 --- a/src/SMAPI.Web/appsettings.json +++ b/src/SMAPI.Web/appsettings.json @@ -19,6 +19,7 @@ "RootUrl": null, // see top note "ModListUrl": null, // see top note "LogParserUrl": null, // see top note + "JsonValidatorUrl": null, // see top note "BetaEnabled": null, // see top note "BetaBlurb": null // see top note }, diff --git a/src/SMAPI.Web/wwwroot/Content/css/json-validator.css b/src/SMAPI.Web/wwwroot/Content/css/json-validator.css new file mode 100644 index 00000000..f9aeb18b --- /dev/null +++ b/src/SMAPI.Web/wwwroot/Content/css/json-validator.css @@ -0,0 +1,98 @@ +/********* +** Main layout +*********/ +#content { + max-width: 100%; +} + +#output { + padding: 10px; + overflow: auto; +} + +#output table td { + font-family: monospace; +} + +#output table tr th, +#output table tr td { + padding: 0 0.75rem; + white-space: pre-wrap; +} + + +/********* +** Result banner +*********/ +.banner { + border: 2px solid gray; + border-radius: 5px; + margin-top: 1em; + padding: 1em; +} + +.banner.success { + border-color: green; + background: #CFC; +} + +.banner.error { + border-color: red; + background: #FCC; +} + +/********* +** Validation results +*********/ +.table { + border-bottom: 1px dashed #888888; + margin-bottom: 5px; +} + +#metadata th, #metadata td { + text-align: left; + padding-right: 0.7em; +} + +.table { + border: 1px solid #000000; + background: #ffffff; + border-radius: 5px; + border-spacing: 1px; + overflow: hidden; + cursor: default; + box-shadow: 1px 1px 1px 1px #dddddd; +} + +.table tr { + background: #eee; +} + +.table tr:nth-child(even) { + background: #fff; +} + +/********* +** Upload form +*********/ +#input { + width: 100%; + height: 20em; + max-height: 70%; + margin: auto; + box-sizing: border-box; + border-radius: 5px; + border: 1px solid #000088; + outline: none; + box-shadow: inset 0px 0px 1px 1px rgba(0, 0, 192, .2); +} + +#submit { + font-size: 1.5em; + border-radius: 5px; + outline: none; + box-shadow: inset 0 0 1px 1px rgba(0, 0, 0, .2); + cursor: pointer; + border: 1px solid #008800; + background-color: #cfc; +} diff --git a/src/SMAPI.Web/wwwroot/Content/css/main.css b/src/SMAPI.Web/wwwroot/Content/css/main.css index 57eeee88..dcc7a798 100644 --- a/src/SMAPI.Web/wwwroot/Content/css/main.css +++ b/src/SMAPI.Web/wwwroot/Content/css/main.css @@ -73,7 +73,7 @@ a { } #sidebar h4 { - margin: 0 0 0.2em 0; + margin: 1.5em 0 0.2em 0; width: 10em; border-bottom: 1px solid #CCC; font-size: 0.8em; diff --git a/src/SMAPI.Web/wwwroot/Content/js/json-validator.js b/src/SMAPI.Web/wwwroot/Content/js/json-validator.js new file mode 100644 index 00000000..3f7a1775 --- /dev/null +++ b/src/SMAPI.Web/wwwroot/Content/js/json-validator.js @@ -0,0 +1,60 @@ +/* globals $ */ + +var smapi = smapi || {}; +smapi.jsonValidator = function (sectionUrl, pasteID) { + /** + * Rebuild the syntax-highlighted element. + */ + var formatCode = function () { + Sunlight.highlightAll(); + }; + + /** + * Initialise the JSON validator page. + */ + var init = function () { + // code formatting + formatCode(); + + // change format + $("#output #format").on("change", function() { + var schemaName = $(this).val(); + location.href = new URL(schemaName + "/" + pasteID, sectionUrl).toString(); + }); + + // upload form + var input = $("#input"); + if (input.length) { + // disable submit if it's empty + var toggleSubmit = function () { + var hasText = !!input.val().trim(); + submit.prop("disabled", !hasText); + }; + input.on("input", toggleSubmit); + toggleSubmit(); + + // drag & drop file + input.on({ + 'dragover dragenter': function (e) { + e.preventDefault(); + e.stopPropagation(); + }, + 'drop': function (e) { + var dataTransfer = e.originalEvent.dataTransfer; + if (dataTransfer && dataTransfer.files.length) { + e.preventDefault(); + e.stopPropagation(); + var file = dataTransfer.files[0]; + var reader = new FileReader(); + reader.onload = $.proxy(function (file, $input, event) { + $input.val(event.target.result); + toggleSubmit(); + }, this, file, $("#input")); + reader.readAsText(file); + } + } + }); + } + }; + init(); +}; diff --git a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js index e87a1a5c..e6c7591c 100644 --- a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js +++ b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js @@ -23,7 +23,7 @@ smapi.logParser = function (data, sectionUrl) { } // set local time started - if(data) + if (data) data.localTimeStarted = ("0" + data.logStarted.getHours()).slice(-2) + ":" + ("0" + data.logStarted.getMinutes()).slice(-2); // init app @@ -100,7 +100,7 @@ smapi.logParser = function (data, sectionUrl) { updateModFilters(); }, - filtersAllow: function(modId, level) { + filtersAllow: function (modId, level) { return this.showMods[modId] !== false && this.showLevels[level] !== false; }, @@ -121,16 +121,15 @@ smapi.logParser = function (data, sectionUrl) { var submit = $("#submit"); // instruction OS chooser - var chooseSystem = function() { + var chooseSystem = function () { systemInstructions.hide(); systemInstructions.filter("[data-os='" + $("input[name='os']:checked").val() + "']").show(); - } + }; systemOptions.on("click", chooseSystem); chooseSystem(); // disable submit if it's empty - var toggleSubmit = function() - { + var toggleSubmit = function () { var hasText = !!input.val().trim(); submit.prop("disabled", !hasText); } @@ -139,18 +138,18 @@ smapi.logParser = function (data, sectionUrl) { // drag & drop file input.on({ - 'dragover dragenter': function(e) { + 'dragover dragenter': function (e) { e.preventDefault(); e.stopPropagation(); }, - 'drop': function(e) { + 'drop': function (e) { var dataTransfer = e.originalEvent.dataTransfer; if (dataTransfer && dataTransfer.files.length) { e.preventDefault(); e.stopPropagation(); var file = dataTransfer.files[0]; var reader = new FileReader(); - reader.onload = $.proxy(function(file, $input, event) { + reader.onload = $.proxy(function (file, $input, event) { $input.val(event.target.result); toggleSubmit(); }, this, file, $("#input")); diff --git a/src/SMAPI.Web/wwwroot/schemas/manifest.json b/src/SMAPI.Web/wwwroot/schemas/manifest.json new file mode 100644 index 00000000..06173333 --- /dev/null +++ b/src/SMAPI.Web/wwwroot/schemas/manifest.json @@ -0,0 +1,120 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://smapi.io/schemas/manifest.json", + "title": "SMAPI manifest", + "description": "Manifest file for a SMAPI mod or content pack", + "type": "object", + "properties": { + "Name": { + "title": "Mod name", + "description": "The mod's display name. SMAPI uses this in player messages, logs, and errors.", + "type": "string", + "examples": [ "Lookup Anything" ] + }, + "Author": { + "title": "Mod author", + "description": "The name of the person who created the mod. Ideally this should include the username used to publish mods.", + "type": "string", + "examples": [ "Pathoschild" ] + }, + "Version": { + "title": "Mod version", + "description": "The mod's semantic version. Make sure you update this for each release! SMAPI uses this for update checks, mod dependencies, and compatibility blacklists (if the mod breaks in a future version of the game).", + "$ref": "#/definitions/SemanticVersion" + }, + "Description": { + "title": "Mod description", + "description": "A short explanation of what your mod does (one or two sentences), shown in the SMAPI log.", + "type": "string", + "examples": [ "View metadata about anything by pressing a button." ] + }, + "UniqueID": { + "title": "Mod unique ID", + "description": "A unique identifier for your mod. The recommended format is \"Username.ModName\", with no spaces or special characters. SMAPI uses this for update checks, mod dependencies, and compatibility blacklists (if the mod breaks in a future version of the game). When another mod needs to reference this mod, it uses the unique ID.", + "$ref": "#/definitions/ModID" + }, + "EntryDll": { + "title": "Mod entry DLL", + "description": "The DLL filename SMAPI should load for this mod. Mutually exclusive with ContentPackFor.", + "type": "string", + "pattern": "^[a-zA-Z0-9_.-]+\\.dll$", + "examples": "LookupAnything.dll" + }, + "ContentPackFor": { + "title": "Content pack for", + "description": "Specifies the mod which can read this content pack.", + "type": "object", + "properties": { + "UniqueID": { + "title": "Required unique ID", + "description": "The unique ID of the mod which can read this content pack.", + "$ref": "#/definitions/ModID" + }, + "MinimumVersion": { + "title": "Required minimum version", + "description": "The minimum semantic version of the mod which can read this content pack, if applicable.", + "$ref": "#/definitions/SemanticVersion" + } + }, + + "required": [ "UniqueID" ] + }, + "Dependencies": { + "title": "Mod dependencies", + "description": "Specifies other mods to load before this mod. If a dependency is required and a player tries to use the mod without the dependency installed, the mod won't be loaded and they'll see a friendly message saying they need to install those.", + "type": "array", + "items": { + "type": "object", + "properties": { + "UniqueID": { + "title": "Dependency unique ID", + "description": "The unique ID of the mod to load first.", + "$ref": "#/definitions/ModID" + }, + "MinimumVersion": { + "title": "Dependency minimum version", + "description": "The minimum semantic version of the mod to load first, if applicable.", + "$ref": "#/definitions/SemanticVersion" + }, + "IsRequired": { + "title": "Dependency is required", + "description": "Whether the dependency is required. Default true if not specified." + } + }, + "required": [ "UniqueID" ] + } + }, + "UpdateKeys": { + "title": "Mod update keys", + "description": "Specifies where SMAPI should check for mod updates, so it can alert the user with a link to your mod page. See https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest#Update_checks for more info.", + "type": "array", + "items": { + "type": "string", + "pattern": "^(Chucklefish:\\d+|Nexus:\\d+|GitHub:[A-Za-z0-9_]+/[A-Za-z0-9_]+|ModDrop:\\d+)$" + } + } + }, + + "required": [ "Name", "Author", "Version", "Description", "UniqueID" ], + "oneOf": [ + { + "required": [ "EntryDll" ] + }, + { + "required": [ "ContentPackFor" ] + } + ], + + "definitions": { + "SemanticVersion": { + "type": "string", + "pattern": "(?>(?0|[1-9]\\d*))\\.(?>(?0|[1-9]\\d*))(?>(?:\\.(?0|[1-9]\\d*))?)(?:-(?(?>[a-zA-Z0-9]+[\\-\\.]?)+))?", // derived from SMAPI.Toolkit.SemanticVersion + "examples": [ "1.0.0", "1.0.1-beta.2" ] + }, + "ModID": { + "type": "string", + "pattern": "^[a-zA-Z0-9_.-]+$", // derived from SMAPI.Toolkit.Utilities.PathUtilities.IsSlug + "examples": [ "Pathoschild.LookupAnything" ] + } + } +} -- cgit From 1b9ce5e64ff248a9cf616f1b7fe8a39a74bd8baa Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 4 Aug 2019 16:49:16 -0400 Subject: format displayed JSON (#654) --- src/SMAPI.Web/Controllers/JsonValidatorController.cs | 3 +++ 1 file changed, 3 insertions(+) (limited to 'src') diff --git a/src/SMAPI.Web/Controllers/JsonValidatorController.cs b/src/SMAPI.Web/Controllers/JsonValidatorController.cs index 9d1685ac..37393a98 100644 --- a/src/SMAPI.Web/Controllers/JsonValidatorController.cs +++ b/src/SMAPI.Web/Controllers/JsonValidatorController.cs @@ -97,6 +97,9 @@ namespace StardewModdingAPI.Web.Controllers return this.View("Index", result.AddErrors(new JsonValidatorErrorModel(ex.LineNumber, ex.Path, ex.Message))); } + // format JSON + result.SetContent(parsed.ToString(Formatting.Indented)); + // skip if no schema selected if (schemaName == "none") return this.View("Index", result); -- cgit From f24e7428df15ee8bcc9a2a45de98363670e72231 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 4 Aug 2019 15:55:24 -0400 Subject: add line highlighting and linking (#654) --- src/SMAPI.Web/Views/JsonValidator/Index.cshtml | 4 +- .../wwwroot/Content/css/json-validator.css | 4 + src/SMAPI.Web/wwwroot/Content/js/json-validator.js | 123 ++++++++++++++++++++- 3 files changed, 127 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Web/Views/JsonValidator/Index.cshtml b/src/SMAPI.Web/Views/JsonValidator/Index.cshtml index cd7ca912..6658e7b9 100644 --- a/src/SMAPI.Web/Views/JsonValidator/Index.cshtml +++ b/src/SMAPI.Web/Views/JsonValidator/Index.cshtml @@ -107,7 +107,7 @@ else if (Model.PasteID != null) @foreach (JsonValidatorErrorModel error in Model.Errors) { - @error.Line + @error.Line @error.Path @error.Message @@ -121,6 +121,6 @@ else if (Model.PasteID != null) }

Raw content

-
@Model.Content
+
@Model.Content
} diff --git a/src/SMAPI.Web/wwwroot/Content/css/json-validator.css b/src/SMAPI.Web/wwwroot/Content/css/json-validator.css index f9aeb18b..d4a43032 100644 --- a/src/SMAPI.Web/wwwroot/Content/css/json-validator.css +++ b/src/SMAPI.Web/wwwroot/Content/css/json-validator.css @@ -72,6 +72,10 @@ background: #fff; } +#output div.sunlight-line-highlight-active { + background-color: #eeeacc; +} + /********* ** Upload form *********/ diff --git a/src/SMAPI.Web/wwwroot/Content/js/json-validator.js b/src/SMAPI.Web/wwwroot/Content/js/json-validator.js index 3f7a1775..265e0c5e 100644 --- a/src/SMAPI.Web/wwwroot/Content/js/json-validator.js +++ b/src/SMAPI.Web/wwwroot/Content/js/json-validator.js @@ -1,20 +1,139 @@ /* globals $ */ var smapi = smapi || {}; + +/** + * Manages the logic for line range selections. + * @param {int} maxLines The maximum number of lines in the content. + */ +smapi.LineNumberRange = function (maxLines) { + var self = this; + + /** + * @var {int} minLine The first line in the selection, or null if no lines selected. + */ + self.minLine = null; + + /** + * @var {int} maxLine The last line in the selection, or null if no lines selected. + */ + self.maxLine = null; + + /** + * Parse line numbers from a URL hash. + * @param {string} hash the URL hash to parse. + */ + self.parseFromUrlHash = function (hash) { + self.minLine = null; + self.maxLine = null; + + // parse hash + var hashParts = hash.match(/^#L(\d+)(?:-L(\d+))?$/); + if (!hashParts || hashParts.length <= 1) + return; + + // extract min/max lines + self.minLine = parseInt(hashParts[1]); + self.maxLine = parseInt(hashParts[2]) || self.minLine; + }; + + /** + * Generate a URL hash for the current line range. + * @returns {string} The generated URL hash. + */ + self.buildHash = function() { + if (!self.minLine) + return ""; + else if (self.minLine === self.maxLine) + return "#L" + self.minLine; + else + return "#L" + self.minLine + "-L" + self.maxLine; + } + + /** + * Get a list of all selected lines. + * @returns {Array} The selected line numbers. + */ + self.getLinesSelected = function() { + // format + if (!self.minLine) + return []; + + var lines = []; + for (var i = self.minLine; i <= self.maxLine; i++) + lines.push(i); + return lines; + }; + + return self; +}; + +/** + * UI logic for the JSON validator page. + * @param {any} sectionUrl The base JSON validator page URL. + * @param {any} pasteID The Pastebin paste ID for the content being viewed, if any. + */ smapi.jsonValidator = function (sectionUrl, pasteID) { + /** + * The original content element. + */ + var originalContent = $("#raw-content").clone(); + + /** + * The currently highlighted lines. + */ + var selection = new smapi.LineNumberRange(); + /** * Rebuild the syntax-highlighted element. */ var formatCode = function () { - Sunlight.highlightAll(); + // reset if needed + $(".sunlight-container").replaceWith(originalContent.clone()); + + // apply default highlighting + Sunlight.highlightAll({ + lineHighlight: selection.getLinesSelected() + }); + + // fix line links + $(".sunlight-line-number-margin a").each(function() { + var link = $(this); + var lineNumber = parseInt(link.text()); + link + .attr("id", "L" + lineNumber) + .attr("href", "#L" + lineNumber) + .removeAttr("name") + .data("line-number", lineNumber); + }); + }; + + /** + * Scroll the page so the selected range is visible. + */ + var scrollToRange = function() { + if (!selection.minLine) + return; + + var targetLine = Math.max(1, selection.minLine - 5); + $("#L" + targetLine).get(0).scrollIntoView(); }; /** * Initialise the JSON validator page. */ var init = function () { - // code formatting + // set initial code formatting + selection.parseFromUrlHash(location.hash); formatCode(); + scrollToRange(); + + // update code formatting on hash change + $(window).on("hashchange", function() { + selection.parseFromUrlHash(location.hash); + formatCode(); + scrollToRange(); + }); // change format $("#output #format").on("change", function() { -- cgit From ee0ff5687d4002aab20cd91fd28d007d916af36c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 4 Aug 2019 18:01:05 -0400 Subject: add user-friendly doc link & error messages, document validator, improve manifest schema (#654) --- docs/technical/web.md | 37 +++++++++++++++- .../Controllers/JsonValidatorController.cs | 49 +++++++++++++++++++++- .../ViewModels/JsonValidator/JsonValidatorModel.cs | 3 ++ src/SMAPI.Web/Views/JsonValidator/Index.cshtml | 5 +++ src/SMAPI.Web/wwwroot/schemas/manifest.json | 48 ++++++++++++++------- 5 files changed, 124 insertions(+), 18 deletions(-) (limited to 'src') diff --git a/docs/technical/web.md b/docs/technical/web.md index 50799e00..9884fefc 100644 --- a/docs/technical/web.md +++ b/docs/technical/web.md @@ -6,6 +6,7 @@ and update check API. ## Contents * [Overview](#overview) * [Log parser](#log-parser) + * [JSON validator](#json-validator) * [Web API](#web-api) * [For SMAPI developers](#for-smapi-developers) * [Local development](#local-development) @@ -16,9 +17,41 @@ The `SMAPI.Web` project provides an API and web UI hosted at `*.smapi.io`. ### Log parser The log parser provides a web UI for uploading, parsing, and sharing SMAPI logs. The logs are -persisted in a compressed form to Pastebin. +persisted in a compressed form to Pastebin. The log parser lives at https://log.smapi.io. + +### JSON validator +The JSON validator provides a web UI for uploading and sharing JSON files, and validating them +as plain JSON or against a predefined format like `manifest.json` or Content Patcher's +`content.json`. The JSON validator lives at https://json.smapi.io. + +Schema files are defined in `wwwroot/schemas` using the [JSON Schema](https://json-schema.org/) +format, with some special properties: +* The root schema may have a `@documentationURL` field, which is the URL to the user-facing + documentation for the format (if any). +* Any part of the schema can define an `@errorMessages` field, which specifies user-friendly errors + which override the auto-generated messages. These are indexed by error type. For example: + ```js + "pattern": "^[a-zA-Z0-9_.-]+\\.dll$", + "@errorMessages": { + "pattern": "Invalid value; must be a filename ending with .dll." + } + ``` + +You can also reference these schemas in your JSON file directly using the `$schema` field, for +text editors that support schema validation. For example: +```js +{ + "$schema": "https://smapi.io/schemas/manifest.json", + "Name": "Some mod", + ... +} +``` + +Current schemas: -The log parser lives at https://log.smapi.io. +format | schema URL +------ | ---------- +[SMAPI `manifest.json`](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest) | https://smapi.io/schemas/manifest.json ### Web API SMAPI provides a web API at `api.smapi.io` for use by SMAPI and external tools. The URL includes a diff --git a/src/SMAPI.Web/Controllers/JsonValidatorController.cs b/src/SMAPI.Web/Controllers/JsonValidatorController.cs index 37393a98..7b755d3b 100644 --- a/src/SMAPI.Web/Controllers/JsonValidatorController.cs +++ b/src/SMAPI.Web/Controllers/JsonValidatorController.cs @@ -113,6 +113,9 @@ namespace StardewModdingAPI.Web.Controllers schema = JSchema.Parse(System.IO.File.ReadAllText(schemaFile.FullName)); } + // get format doc URL + result.FormatUrl = this.GetExtensionField(schema, "@documentationUrl"); + // validate JSON parsed.IsValid(schema, out IList rawErrors); var errors = rawErrors @@ -172,13 +175,22 @@ namespace StardewModdingAPI.Web.Controllers /// The indentation level to apply for inner errors. private string GetFlattenedError(ValidationError error, int indent = 0) { + // get override error + string message = this.GetOverrideError(error.Schema, error.ErrorType); + if (message != null) + return message; + // get friendly representation of main error - string message = error.Message; + message = error.Message; switch (error.ErrorType) { case ErrorType.Enum: message = $"Invalid value. Found '{error.Value}', but expected one of '{string.Join("', '", error.Schema.Enum)}'."; break; + + case ErrorType.Required: + message = $"Missing required fields: {string.Join(", ", (List)error.Value)}."; + break; } // add inner errors @@ -216,5 +228,40 @@ namespace StardewModdingAPI.Web.Controllers return null; } + + /// Get an override error from the JSON schema, if any. + /// The schema or subschema that raised the error. + /// The error type. + private string GetOverrideError(JSchema schema, ErrorType errorType) + { + // get override errors + IDictionary errors = this.GetExtensionField>(schema, "@errorMessages"); + if (errors == null) + return null; + errors = new Dictionary(errors, StringComparer.InvariantCultureIgnoreCase); + + // get matching error + return errors.TryGetValue(errorType.ToString(), out string errorPhrase) + ? errorPhrase + : null; + } + + /// Get an extension field from a JSON schema. + /// The field type. + /// The schema whose extension fields to search. + /// The case-insensitive field key. + private T GetExtensionField(JSchema schema, string key) + { + if (schema.ExtensionData != null) + { + foreach (var pair in schema.ExtensionData) + { + if (pair.Key.Equals(key, StringComparison.InvariantCultureIgnoreCase)) + return pair.Value.ToObject(); + } + } + + return default; + } } } diff --git a/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorModel.cs b/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorModel.cs index 4c122d4f..2d13bf23 100644 --- a/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorModel.cs +++ b/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorModel.cs @@ -33,6 +33,9 @@ namespace StardewModdingAPI.Web.ViewModels.JsonValidator /// An error which occurred while parsing the JSON. public string ParseError { get; set; } + /// A web URL to the user-facing format documentation. + public string FormatUrl { get; set; } + /********* ** Public methods diff --git a/src/SMAPI.Web/Views/JsonValidator/Index.cshtml b/src/SMAPI.Web/Views/JsonValidator/Index.cshtml index 6658e7b9..5c3168e5 100644 --- a/src/SMAPI.Web/Views/JsonValidator/Index.cshtml +++ b/src/SMAPI.Web/Views/JsonValidator/Index.cshtml @@ -95,6 +95,11 @@ else if (Model.PasteID != null)

Validation errors

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

See format documentation.

+ } + @if (Model.Errors.Any()) { diff --git a/src/SMAPI.Web/wwwroot/schemas/manifest.json b/src/SMAPI.Web/wwwroot/schemas/manifest.json index 06173333..804eb53d 100644 --- a/src/SMAPI.Web/wwwroot/schemas/manifest.json +++ b/src/SMAPI.Web/wwwroot/schemas/manifest.json @@ -3,6 +3,7 @@ "$id": "https://smapi.io/schemas/manifest.json", "title": "SMAPI manifest", "description": "Manifest file for a SMAPI mod or content pack", + "@documentationUrl": "https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest", "type": "object", "properties": { "Name": { @@ -38,7 +39,10 @@ "description": "The DLL filename SMAPI should load for this mod. Mutually exclusive with ContentPackFor.", "type": "string", "pattern": "^[a-zA-Z0-9_.-]+\\.dll$", - "examples": "LookupAnything.dll" + "examples": "LookupAnything.dll", + "@errorMessages": { + "pattern": "Invalid value; must be a filename ending with .dll." + } }, "ContentPackFor": { "title": "Content pack for", @@ -59,12 +63,17 @@ "required": [ "UniqueID" ] }, + "MinimumApiVersion": { + "title": "Minimum API version", + "description": "The minimum SMAPI version needed to use this mod. If a player tries to use the mod with an older SMAPI version, they'll see a friendly message saying they need to update SMAPI. This also serves as a proxy for the minimum game version, since SMAPI itself enforces a minimum game version.", + "$ref": "#/definitions/SemanticVersion" + }, "Dependencies": { "title": "Mod dependencies", "description": "Specifies other mods to load before this mod. If a dependency is required and a player tries to use the mod without the dependency installed, the mod won't be loaded and they'll see a friendly message saying they need to install those.", "type": "array", "items": { - "type": "object", + "type": "object", "properties": { "UniqueID": { "title": "Dependency unique ID", @@ -90,8 +99,26 @@ "type": "array", "items": { "type": "string", - "pattern": "^(Chucklefish:\\d+|Nexus:\\d+|GitHub:[A-Za-z0-9_]+/[A-Za-z0-9_]+|ModDrop:\\d+)$" + "pattern": "^(Chucklefish:\\d+|Nexus:\\d+|GitHub:[A-Za-z0-9_]+/[A-Za-z0-9_]+|ModDrop:\\d+)$", + "@errorMessages": { + "pattern": "Invalid update key; see https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest#Update_checks for more info." + } + } + } + }, + "definitions": { + "SemanticVersion": { + "type": "string", + "pattern": "^(?>(?0|[1-9]\\d*))\\.(?>(?0|[1-9]\\d*))(?>(?:\\.(?0|[1-9]\\d*))?)(?:-(?(?>[a-zA-Z0-9]+[\\-\\.]?)+))?$", // derived from SMAPI.Toolkit.SemanticVersion + "examples": [ "1.0.0", "1.0.1-beta.2" ], + "@errorMessages": { + "pattern": "Invalid semantic version; must be formatted like 1.2.0 or 1.2.0-prerelease.tags. See https://semver.org/ for more info." } + }, + "ModID": { + "type": "string", + "pattern": "^[a-zA-Z0-9_.-]+$", // derived from SMAPI.Toolkit.Utilities.PathUtilities.IsSlug + "examples": [ "Pathoschild.LookupAnything" ] } }, @@ -104,17 +131,8 @@ "required": [ "ContentPackFor" ] } ], - - "definitions": { - "SemanticVersion": { - "type": "string", - "pattern": "(?>(?0|[1-9]\\d*))\\.(?>(?0|[1-9]\\d*))(?>(?:\\.(?0|[1-9]\\d*))?)(?:-(?(?>[a-zA-Z0-9]+[\\-\\.]?)+))?", // derived from SMAPI.Toolkit.SemanticVersion - "examples": [ "1.0.0", "1.0.1-beta.2" ] - }, - "ModID": { - "type": "string", - "pattern": "^[a-zA-Z0-9_.-]+$", // derived from SMAPI.Toolkit.Utilities.PathUtilities.IsSlug - "examples": [ "Pathoschild.LookupAnything" ] - } + "additionalProperties": false, + "@errorMessages": { + "oneOf": "Can't specify both EntryDll or ContentPackFor, they're mutually exclusive." } } -- cgit From 5e8991bfcf7f287f595e858c34b8ac1a92c42b9b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 4 Aug 2019 18:55:06 -0400 Subject: tweak button names, update release notes (#654) --- docs/release-notes.md | 4 ++++ src/SMAPI.Web/Views/JsonValidator/Index.cshtml | 2 +- src/SMAPI.Web/Views/LogParser/Index.cshtml | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 9ee8ae8f..9ec33000 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -36,6 +36,10 @@ These changes have not been released yet. * Clicking a mod link now automatically adds it to the visible mods when the list is filtered. * Added metadata links and dev notes (if any) to advanced info. +* For the JSON validator: + * Added JSON validator at [json.smapi.io](https://json.smapi.io), which lets you validate a JSON file against predefined mod formats. + * Added support for the `manifest.json` format. + * For modders: * Mods are now loaded much earlier in the game launch. This lets mods intercept any content asset, but the game is not fully initialised when `Entry` is called (use the `GameLaunched` event if you need to run code when the game is initialised). * Added support for content pack translations. diff --git a/src/SMAPI.Web/Views/JsonValidator/Index.cshtml b/src/SMAPI.Web/Views/JsonValidator/Index.cshtml index 5c3168e5..1cf35e39 100644 --- a/src/SMAPI.Web/Views/JsonValidator/Index.cshtml +++ b/src/SMAPI.Web/Views/JsonValidator/Index.cshtml @@ -72,7 +72,7 @@ else if (Model.PasteID != null)
  • Click this button:
    - +
  • diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index 1b40cfa9..e974c308 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -118,7 +118,7 @@ else if (Model.ParsedLog?.IsValid == true)
  • Click this button:
    - +
  • On the new page, copy the URL and send it to the person helping you.
  • -- cgit From 84ad8b2a92eac9155cada821c57d62a517b958a8 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 4 Aug 2019 20:13:10 -0400 Subject: fix manifest error if neither EntryDll nor ContentPackFor are specified (#654) --- docs/technical/web.md | 10 ++++++++- .../Controllers/JsonValidatorController.cs | 26 +++++++++++++++++----- src/SMAPI.Web/wwwroot/schemas/manifest.json | 3 ++- 3 files changed, 31 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/docs/technical/web.md b/docs/technical/web.md index 9884fefc..0d2039d8 100644 --- a/docs/technical/web.md +++ b/docs/technical/web.md @@ -29,13 +29,21 @@ format, with some special properties: * The root schema may have a `@documentationURL` field, which is the URL to the user-facing documentation for the format (if any). * Any part of the schema can define an `@errorMessages` field, which specifies user-friendly errors - which override the auto-generated messages. These are indexed by error type. For example: + which override the auto-generated messages. These can be indexed by error type: ```js "pattern": "^[a-zA-Z0-9_.-]+\\.dll$", "@errorMessages": { "pattern": "Invalid value; must be a filename ending with .dll." } ``` + ...or by error type and a regular expression applied to the default message (not recommended + unless the previous form doesn't work, since it's more likely to break in future versions): + ```js + "@errorMessages": { + "oneOf:valid against no schemas": "Missing required field: EntryDll or ContentPackFor.", + "oneOf:valid against more than one schema": "Can't specify both EntryDll or ContentPackFor, they're mutually exclusive." + } + ``` You can also reference these schemas in your JSON file directly using the `$schema` field, for text editors that support schema validation. For example: diff --git a/src/SMAPI.Web/Controllers/JsonValidatorController.cs b/src/SMAPI.Web/Controllers/JsonValidatorController.cs index 7b755d3b..b69a1006 100644 --- a/src/SMAPI.Web/Controllers/JsonValidatorController.cs +++ b/src/SMAPI.Web/Controllers/JsonValidatorController.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; @@ -176,7 +177,7 @@ namespace StardewModdingAPI.Web.Controllers private string GetFlattenedError(ValidationError error, int indent = 0) { // get override error - string message = this.GetOverrideError(error.Schema, error.ErrorType); + string message = this.GetOverrideError(error.Schema, error.ErrorType, error.Message); if (message != null) return message; @@ -232,7 +233,8 @@ namespace StardewModdingAPI.Web.Controllers /// Get an override error from the JSON schema, if any. /// The schema or subschema that raised the error. /// The error type. - private string GetOverrideError(JSchema schema, ErrorType errorType) + /// The error message. + private string GetOverrideError(JSchema schema, ErrorType errorType, string message) { // get override errors IDictionary errors = this.GetExtensionField>(schema, "@errorMessages"); @@ -240,10 +242,22 @@ namespace StardewModdingAPI.Web.Controllers return null; errors = new Dictionary(errors, StringComparer.InvariantCultureIgnoreCase); - // get matching error - return errors.TryGetValue(errorType.ToString(), out string errorPhrase) - ? errorPhrase - : null; + // match error by type and message + foreach (var pair in errors) + { + if (!pair.Key.Contains(":")) + continue; + + string[] parts = pair.Key.Split(':', 2); + if (parts[0].Equals(errorType.ToString(), StringComparison.InvariantCultureIgnoreCase) && Regex.IsMatch(message, parts[1])) + return pair.Value; + } + + // match by type + if (errors.TryGetValue(errorType.ToString(), out string error)) + return error; + + return null; } /// Get an extension field from a JSON schema. diff --git a/src/SMAPI.Web/wwwroot/schemas/manifest.json b/src/SMAPI.Web/wwwroot/schemas/manifest.json index 804eb53d..41d1ec2d 100644 --- a/src/SMAPI.Web/wwwroot/schemas/manifest.json +++ b/src/SMAPI.Web/wwwroot/schemas/manifest.json @@ -133,6 +133,7 @@ ], "additionalProperties": false, "@errorMessages": { - "oneOf": "Can't specify both EntryDll or ContentPackFor, they're mutually exclusive." + "oneOf:valid against no schemas": "Missing required field: EntryDll or ContentPackFor.", + "oneOf:valid against more than one schema": "Can't specify both EntryDll or ContentPackFor, they're mutually exclusive." } } -- cgit From c785572fdd07df17487fedd67551cb9e5ff60a2d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 5 Aug 2019 00:48:52 -0400 Subject: raise JSON validation error for duplicate fields (#654) --- src/SMAPI.Web/Controllers/JsonValidatorController.cs | 5 ++++- src/SMAPI.Web/wwwroot/schemas/manifest.json | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Web/Controllers/JsonValidatorController.cs b/src/SMAPI.Web/Controllers/JsonValidatorController.cs index b69a1006..41c07cee 100644 --- a/src/SMAPI.Web/Controllers/JsonValidatorController.cs +++ b/src/SMAPI.Web/Controllers/JsonValidatorController.cs @@ -91,7 +91,10 @@ namespace StardewModdingAPI.Web.Controllers JToken parsed; try { - parsed = JToken.Parse(paste.Content); + parsed = JToken.Parse(paste.Content, new JsonLoadSettings + { + DuplicatePropertyNameHandling = DuplicatePropertyNameHandling.Error + }); } catch (JsonReaderException ex) { diff --git a/src/SMAPI.Web/wwwroot/schemas/manifest.json b/src/SMAPI.Web/wwwroot/schemas/manifest.json index 41d1ec2d..d4727eda 100644 --- a/src/SMAPI.Web/wwwroot/schemas/manifest.json +++ b/src/SMAPI.Web/wwwroot/schemas/manifest.json @@ -134,6 +134,6 @@ "additionalProperties": false, "@errorMessages": { "oneOf:valid against no schemas": "Missing required field: EntryDll or ContentPackFor.", - "oneOf:valid against more than one schema": "Can't specify both EntryDll or ContentPackFor, they're mutually exclusive." + "oneOf:valid against more than one schema": "Can't specify both EntryDll and ContentPackFor, they're mutually exclusive." } } -- cgit From 093c68cac4839603e8c1fcc456d86bcfdf309b60 Mon Sep 17 00:00:00 2001 From: TehPers Date: Mon, 5 Aug 2019 19:39:42 -0700 Subject: Fixed --- src/SMAPI.Web/wwwroot/schemas/content-patcher.json | 358 +++++++++++++++++++++ 1 file changed, 358 insertions(+) create mode 100644 src/SMAPI.Web/wwwroot/schemas/content-patcher.json (limited to 'src') diff --git a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json new file mode 100644 index 00000000..e4008abb --- /dev/null +++ b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json @@ -0,0 +1,358 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://raw.githubusercontent.com/Pathoschild/SMAPI/develop/src/SMAPI.Web/wwwroot/schemas/content-patcher.json", + "title": "Content Patcher", + "description": "Content Patcher content file for mods", + "type": "object", + "definitions": { + "Version": { + "type": "string", + "pattern": "\\d+\\.\\d+" + }, + "Change": { + "properties": { + "Action": { + "title": "Action", + "description": "The kind of change to make.", + "type": "string", + "enum": ["Load", "EditImage", "EditData", "EditMap"] + }, + "Target": { + "title": "Target asset", + "description": "The game asset you want to patch (or multiple comma-delimited assets). This is the file path inside your game's Content folder, without the file extension or language (like Animals/Dinosaur to edit Content/Animals/Dinosaur.xnb). This field supports tokens and capitalisation doesn't matter. Your changes are applied in all languages unless you specify a language condition.", + "type": "string" + }, + "LogName": { + "title": "Patch log name", + "description": "A name for this patch shown in log messages. This is very useful for understanding errors; if not specified, will default to a name like entry #14 (EditImage Animals/Dinosaurs).", + "type": "string" + }, + "Enabled": { + "title": "Enabled", + "description": "Whether to apply this patch. Default true. This fields supports immutable tokens (e.g. config tokens) if they return true/false.", + "anyOf": [ + { + "type": "string", + "enum": ["true", "false"] + }, + { + "type": "string", + "pattern": "\\{\\{[^{}]+\\}\\}" + } + ] + }, + "When": { + "title": "When", + "description": "Only apply the patch if the given conditions match.", + "$ref": "#/definitions/Condition" + } + }, + "required": ["Action", "Target"], + "allOf": [ + { + "if": { + "properties": { + "Action": { + "const": "Load" + } + } + }, + "then": { + "$ref": "#/definitions/LoadChange" + } + }, + { + "if": { + "properties": { + "Action": { + "const": "EditImage" + } + } + }, + "then": { + "$ref": "#/definitions/EditImageChange" + } + }, + { + "if": { + "properties": { + "Action": { + "const": "EditData" + } + } + }, + "then": { + "$ref": "#/definitions/EditDataChange" + } + }, + { + "if": { + "properties": { + "Action": { + "const": "EditMap" + } + } + }, + "then": { + "$ref": "#/definitions/EditMapChange" + } + } + ] + }, + "LoadChange": { + "properties": { + "FromFile": { + "$ref": "#/definitions/FromFile" + } + }, + "required": ["FromFile"] + }, + "EditImageChange": { + "properties": { + "FromFile": { + "$ref": "#/definitions/FromFile" + }, + "FromArea": { + "title": "Source area", + "description": "The part of the source image to copy. Defaults to the whole source image.", + "$ref": "#/definitions/Rectangle" + }, + "ToArea": { + "title": "Destination area", + "description": "The part of the target image to replace. Defaults to the FromArea size starting from the top-left corner.", + "$ref": "#/definitions/Rectangle" + }, + "PatchMode": { + "title": "Patch mode", + "description": "How to apply FromArea to ToArea. Defaults to Replace.", + "type": "string", + "enum": ["Replace", "Overlay"] + } + }, + "required": ["FromFile"] + }, + "EditDataChange": { + "properties": { + "Fields": { + "title": "Fields", + "description": "The individual fields you want to change for existing entries. This field supports tokens in field keys and values. The key for each field is the field index (starting at zero) for a slash-delimited string, or the field name for an object.", + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "Entries": { + "title": "Entries", + "description": "The entries in the data file you want to add, replace, or delete. If you only want to change a few fields, use Fields instead for best compatibility with other mods. To add an entry, just specify a key that doesn't exist; to delete an entry, set the value to null (like \"some key\": null). This field supports tokens in entry keys and values.\nCaution: some XNB files have extra fields at the end for translations; when adding or replacing an entry for all locales, make sure you include the extra fields to avoid errors for non-English players." + }, + "MoveEntries": { + "type": "array", + "items": { + "type": "object", + "properties": { + "ID": { + "title": "ID", + "description": "The ID of the entry to move", + "type": "string" + } + }, + "anyOf": [ + { + "properties": { + "ID": {}, + "BeforeID": { + "title": "Before ID", + "description": "Move entry so it's right before this ID", + "type": "string" + } + }, + "required": ["ID", "BeforeID"], + "additionalProperties": false + }, + { + "properties": { + "ID": {}, + "AfterID": { + "title": "After ID", + "description": "Move entry so it's right after this ID", + "type": "string" + } + }, + "required": ["ID", "AfterID"], + "additionalProperties": false + }, + { + "properties": { + "ID": {}, + "ToPosition": { + "title": "To position", + "description": "Move entry so it's right at this position", + "enum": ["Top", "Bottom"] + } + }, + "required": ["ID", "ToPosition"], + "additionalProperties": false + } + ], + "required": ["ID"] + } + } + } + }, + "EditMapChange": { + "properties": { + "FromFile": { + "description": "The relative path to the map in your content pack folder from which to copy (like assets/town.tbin). This can be a .tbin or .xnb file. This field supports tokens and capitalisation doesn't matter.\nContent Patcher will handle tilesheets referenced by the FromFile map for you if it's a .tbin file:\n - If a tilesheet isn't referenced by the target map, Content Patcher will add it for you (with a z_ ID prefix to avoid conflicts with hardcoded game logic). If the source map has a custom version of a tilesheet that's already referenced, it'll be added as a separate tilesheet only used by your tiles.\n - If you include the tilesheet file in your mod folder, Content Patcher will use that one automatically; otherwise it will be loaded from the game's Content/Maps folder.", + "$ref": "#/definitions/FromFile" + }, + "FromArea": { + "title": "Source area", + "description": "The part of the source map to copy. Defaults to the whole source map.", + "$ref": "#/definitions/Rectangle" + }, + "ToArea": { + "title": "Target area", + "description": "The part of the target map to replace.", + "$ref": "#/definitions/Rectangle" + } + }, + "required": ["FromFile", "ToArea"] + }, + "Config": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "AllowValues": { + "title": "Allowed values", + "description": "The values the player can provide, as a comma-delimited string. If omitted, any value is allowed.\nTip: for a boolean flag, use \"true, false\".", + "type": "string" + }, + "AllowBlank": { + "title": "Allow blank", + "description": "Whether the field can be left blank. If false or omitted, blank fields will be replaced with the default value.", + "type": "boolean" + }, + "AllowMultiple": { + "title": "Allow multiple values", + "description": "Whether the player can specify multiple comma-delimited values. Default false.", + "type": "boolean" + }, + "Default": { + "title": "Default value", + "description": "The default values when the field is missing. Can contain multiple comma-delimited values if AllowMultiple is true. If omitted, blank fields are left blank.", + "type": "string" + } + }, + "if": { + "properties": { + "AllowBlank": { + "const": false + } + }, + "required": ["AllowBlank"] + }, + "then": { + "required": ["Default"] + } + } + }, + "Condition": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "DynamicToken": { + "type": "object", + "properties": { + "Name": { + "title": "Name", + "description": "The name of the token to use for tokens & condition.", + "type": "string" + }, + "Value": { + "title": "Token value", + "description": "The value(s) to set. This can be a comma-delimited value to give it multiple values. If any block for a token name has multiple values, it will only be usable in conditions. This field supports tokens, including dynamic tokens defined before this entry.", + "type": "string" + }, + "When": { + "title": "When", + "description": "Only set the value if the given conditions match. If not specified, always matches.", + "$ref": "#/definitions/Condition" + } + }, + "required": ["Name", "Value"] + }, + "FromFile": { + "title": "Source file", + "description": "The relative file path in your content pack folder to load instead (like assets/dinosaur.png). This can be a .json (data), .png (image), .tbin (map), or .xnb file. This field supports tokens and capitalisation doesn't matter.", + "type": "string", + "pattern": ".*\\.(json|png|tbin|xnb)$|.*\\{\\{[^}]+}}$" + }, + "Rectangle": { + "type": "object", + "properties": { + "X": { + "title": "X-Coordinate", + "description": "Location in pixels of the top-left of the rectangle", + "type": "number", + "multipleOf": 1 + }, + "Y": { + "title": "Y-Coordinate", + "description": "Location in pixels of the top-left of the rectangle", + "type": "number", + "multipleOf": 1 + }, + "Width": { + "title": "Width", + "description": "The width of the rectangle", + "type": "number", + "multipleOf": 1 + }, + "Height": { + "title": "Height", + "description": "The height of the rectangle", + "type": "number", + "multipleOf": 1 + } + } + } + }, + "properties": { + "Format": { + "title": "Format version", + "description": "The format version. You should always use the latest version to use the latest features and avoid obsolete behavior.", + "$ref": "#/definitions/Version" + }, + "Changes": { + "title": "Changes", + "description": "The changes you want to make. Each entry is called a patch, and describes a specific action to perform: replace this file, copy this image into the file, etc. You can list any number of patches.", + "type": "array", + "items": { + "$ref": "#/definitions/Change" + } + }, + "ConfigSchema": { + "title": "Config schema", + "description": "Defines the config.json format, to support more complex mods.", + "$ref": "#/definitions/Config" + }, + "DynamicTokens": { + "title": "Dynamic tokens", + "description": "Custom tokens that you can use.", + "type": "array", + "items": { + "$ref": "#/definitions/DynamicToken" + } + }, + "$schema": { + "title": "Schema", + "description": "The schema this JSON should follow. Useful for JSON validation tools.", + "type": "string", + "format": "uri" + } + }, + "required": ["Format", "Changes"] +} -- cgit From 31990916198708dc89343066faaf34703f2c1cf1 Mon Sep 17 00:00:00 2001 From: TehPers Date: Mon, 5 Aug 2019 19:47:26 -0700 Subject: Updated $id --- src/SMAPI.Web/wwwroot/schemas/content-patcher.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json index e4008abb..3ef5d263 100644 --- a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json +++ b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://raw.githubusercontent.com/Pathoschild/SMAPI/develop/src/SMAPI.Web/wwwroot/schemas/content-patcher.json", + "$id": "https://smapi.io/schemas/content-patcher.json", "title": "Content Patcher", "description": "Content Patcher content file for mods", "type": "object", -- cgit From 5d06d0b83692e1209a8338ee3b3a2c9f0aa776ca Mon Sep 17 00:00:00 2001 From: TehPers Date: Mon, 5 Aug 2019 19:51:36 -0700 Subject: Updated title with feedback from Cat --- src/SMAPI.Web/wwwroot/schemas/content-patcher.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json index 3ef5d263..e5810871 100644 --- a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json +++ b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://smapi.io/schemas/content-patcher.json", - "title": "Content Patcher", + "title": "Content Patcher content pack", "description": "Content Patcher content file for mods", "type": "object", "definitions": { -- cgit From 3331beb17a7bda439b281e7507ae187c68b6359c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 5 Aug 2019 23:30:11 -0400 Subject: integrate Content Patcher schema into validator, update docs (#654) --- docs/release-notes.md | 1 + docs/technical/web.md | 1 + .../Controllers/JsonValidatorController.cs | 3 +- src/SMAPI.Web/wwwroot/schemas/content-patcher.json | 33 +++++++++++----------- 4 files changed, 21 insertions(+), 17 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 9ec33000..9ce88db1 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -39,6 +39,7 @@ These changes have not been released yet. * For the JSON validator: * Added JSON validator at [json.smapi.io](https://json.smapi.io), which lets you validate a JSON file against predefined mod formats. * Added support for the `manifest.json` format. + * Added support for Content Patcher's `content.json` format (thanks to TehPers!). * For modders: * Mods are now loaded much earlier in the game launch. This lets mods intercept any content asset, but the game is not fully initialised when `Entry` is called (use the `GameLaunched` event if you need to run code when the game is initialised). diff --git a/docs/technical/web.md b/docs/technical/web.md index 0d2039d8..a008fe72 100644 --- a/docs/technical/web.md +++ b/docs/technical/web.md @@ -60,6 +60,7 @@ Current schemas: format | schema URL ------ | ---------- [SMAPI `manifest.json`](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest) | https://smapi.io/schemas/manifest.json +[Content Patcher `content.json`](https://github.com/Pathoschild/StardewMods/tree/develop/ContentPatcher#readme) | https://smapi.io/schemas/content-patcher.json ### Web API SMAPI provides a web API at `api.smapi.io` for use by SMAPI and external tools. The URL includes a diff --git a/src/SMAPI.Web/Controllers/JsonValidatorController.cs b/src/SMAPI.Web/Controllers/JsonValidatorController.cs index 41c07cee..9ded9c0d 100644 --- a/src/SMAPI.Web/Controllers/JsonValidatorController.cs +++ b/src/SMAPI.Web/Controllers/JsonValidatorController.cs @@ -39,7 +39,8 @@ namespace StardewModdingAPI.Web.Controllers private readonly IDictionary SchemaFormats = new Dictionary { ["none"] = "None", - ["manifest"] = "Manifest" + ["manifest"] = "Manifest", + ["content-patcher"] = "Content Patcher" }; /// The schema ID to use if none was specified. diff --git a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json index e5810871..8dae5a98 100644 --- a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json +++ b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json @@ -3,6 +3,7 @@ "$id": "https://smapi.io/schemas/content-patcher.json", "title": "Content Patcher content pack", "description": "Content Patcher content file for mods", + "@documentationUrl": "https://github.com/Pathoschild/StardewMods/tree/develop/ContentPatcher#readme", "type": "object", "definitions": { "Version": { @@ -15,7 +16,7 @@ "title": "Action", "description": "The kind of change to make.", "type": "string", - "enum": ["Load", "EditImage", "EditData", "EditMap"] + "enum": [ "Load", "EditImage", "EditData", "EditMap" ] }, "Target": { "title": "Target asset", @@ -33,7 +34,7 @@ "anyOf": [ { "type": "string", - "enum": ["true", "false"] + "enum": [ "true", "false" ] }, { "type": "string", @@ -47,7 +48,7 @@ "$ref": "#/definitions/Condition" } }, - "required": ["Action", "Target"], + "required": [ "Action", "Target" ], "allOf": [ { "if": { @@ -105,7 +106,7 @@ "$ref": "#/definitions/FromFile" } }, - "required": ["FromFile"] + "required": [ "FromFile" ] }, "EditImageChange": { "properties": { @@ -126,10 +127,10 @@ "title": "Patch mode", "description": "How to apply FromArea to ToArea. Defaults to Replace.", "type": "string", - "enum": ["Replace", "Overlay"] + "enum": [ "Replace", "Overlay" ] } }, - "required": ["FromFile"] + "required": [ "FromFile" ] }, "EditDataChange": { "properties": { @@ -166,7 +167,7 @@ "type": "string" } }, - "required": ["ID", "BeforeID"], + "required": [ "ID", "BeforeID" ], "additionalProperties": false }, { @@ -178,7 +179,7 @@ "type": "string" } }, - "required": ["ID", "AfterID"], + "required": [ "ID", "AfterID" ], "additionalProperties": false }, { @@ -187,14 +188,14 @@ "ToPosition": { "title": "To position", "description": "Move entry so it's right at this position", - "enum": ["Top", "Bottom"] + "enum": [ "Top", "Bottom" ] } }, - "required": ["ID", "ToPosition"], + "required": [ "ID", "ToPosition" ], "additionalProperties": false } ], - "required": ["ID"] + "required": [ "ID" ] } } } @@ -216,7 +217,7 @@ "$ref": "#/definitions/Rectangle" } }, - "required": ["FromFile", "ToArea"] + "required": [ "FromFile", "ToArea" ] }, "Config": { "type": "object", @@ -250,10 +251,10 @@ "const": false } }, - "required": ["AllowBlank"] + "required": [ "AllowBlank" ] }, "then": { - "required": ["Default"] + "required": [ "Default" ] } } }, @@ -282,7 +283,7 @@ "$ref": "#/definitions/Condition" } }, - "required": ["Name", "Value"] + "required": [ "Name", "Value" ] }, "FromFile": { "title": "Source file", @@ -354,5 +355,5 @@ "format": "uri" } }, - "required": ["Format", "Changes"] + "required": [ "Format", "Changes" ] } -- cgit From 22480d25b9ba15df8cd26e788cd4a2d73f002a0d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 6 Aug 2019 00:15:21 -0400 Subject: restructure Content Patcher schema a bit (#654) --- src/SMAPI.Web/wwwroot/schemas/content-patcher.json | 508 ++++++++++----------- 1 file changed, 238 insertions(+), 270 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json index 8dae5a98..25cba70e 100644 --- a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json +++ b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json @@ -5,221 +5,17 @@ "description": "Content Patcher content file for mods", "@documentationUrl": "https://github.com/Pathoschild/StardewMods/tree/develop/ContentPatcher#readme", "type": "object", - "definitions": { - "Version": { + + "properties": { + "Format": { + "title": "Format version", + "description": "The format version. You should always use the latest version to use the latest features and avoid obsolete behavior.", "type": "string", "pattern": "\\d+\\.\\d+" }, - "Change": { - "properties": { - "Action": { - "title": "Action", - "description": "The kind of change to make.", - "type": "string", - "enum": [ "Load", "EditImage", "EditData", "EditMap" ] - }, - "Target": { - "title": "Target asset", - "description": "The game asset you want to patch (or multiple comma-delimited assets). This is the file path inside your game's Content folder, without the file extension or language (like Animals/Dinosaur to edit Content/Animals/Dinosaur.xnb). This field supports tokens and capitalisation doesn't matter. Your changes are applied in all languages unless you specify a language condition.", - "type": "string" - }, - "LogName": { - "title": "Patch log name", - "description": "A name for this patch shown in log messages. This is very useful for understanding errors; if not specified, will default to a name like entry #14 (EditImage Animals/Dinosaurs).", - "type": "string" - }, - "Enabled": { - "title": "Enabled", - "description": "Whether to apply this patch. Default true. This fields supports immutable tokens (e.g. config tokens) if they return true/false.", - "anyOf": [ - { - "type": "string", - "enum": [ "true", "false" ] - }, - { - "type": "string", - "pattern": "\\{\\{[^{}]+\\}\\}" - } - ] - }, - "When": { - "title": "When", - "description": "Only apply the patch if the given conditions match.", - "$ref": "#/definitions/Condition" - } - }, - "required": [ "Action", "Target" ], - "allOf": [ - { - "if": { - "properties": { - "Action": { - "const": "Load" - } - } - }, - "then": { - "$ref": "#/definitions/LoadChange" - } - }, - { - "if": { - "properties": { - "Action": { - "const": "EditImage" - } - } - }, - "then": { - "$ref": "#/definitions/EditImageChange" - } - }, - { - "if": { - "properties": { - "Action": { - "const": "EditData" - } - } - }, - "then": { - "$ref": "#/definitions/EditDataChange" - } - }, - { - "if": { - "properties": { - "Action": { - "const": "EditMap" - } - } - }, - "then": { - "$ref": "#/definitions/EditMapChange" - } - } - ] - }, - "LoadChange": { - "properties": { - "FromFile": { - "$ref": "#/definitions/FromFile" - } - }, - "required": [ "FromFile" ] - }, - "EditImageChange": { - "properties": { - "FromFile": { - "$ref": "#/definitions/FromFile" - }, - "FromArea": { - "title": "Source area", - "description": "The part of the source image to copy. Defaults to the whole source image.", - "$ref": "#/definitions/Rectangle" - }, - "ToArea": { - "title": "Destination area", - "description": "The part of the target image to replace. Defaults to the FromArea size starting from the top-left corner.", - "$ref": "#/definitions/Rectangle" - }, - "PatchMode": { - "title": "Patch mode", - "description": "How to apply FromArea to ToArea. Defaults to Replace.", - "type": "string", - "enum": [ "Replace", "Overlay" ] - } - }, - "required": [ "FromFile" ] - }, - "EditDataChange": { - "properties": { - "Fields": { - "title": "Fields", - "description": "The individual fields you want to change for existing entries. This field supports tokens in field keys and values. The key for each field is the field index (starting at zero) for a slash-delimited string, or the field name for an object.", - "type": "object", - "additionalProperties": { - "type": "object" - } - }, - "Entries": { - "title": "Entries", - "description": "The entries in the data file you want to add, replace, or delete. If you only want to change a few fields, use Fields instead for best compatibility with other mods. To add an entry, just specify a key that doesn't exist; to delete an entry, set the value to null (like \"some key\": null). This field supports tokens in entry keys and values.\nCaution: some XNB files have extra fields at the end for translations; when adding or replacing an entry for all locales, make sure you include the extra fields to avoid errors for non-English players." - }, - "MoveEntries": { - "type": "array", - "items": { - "type": "object", - "properties": { - "ID": { - "title": "ID", - "description": "The ID of the entry to move", - "type": "string" - } - }, - "anyOf": [ - { - "properties": { - "ID": {}, - "BeforeID": { - "title": "Before ID", - "description": "Move entry so it's right before this ID", - "type": "string" - } - }, - "required": [ "ID", "BeforeID" ], - "additionalProperties": false - }, - { - "properties": { - "ID": {}, - "AfterID": { - "title": "After ID", - "description": "Move entry so it's right after this ID", - "type": "string" - } - }, - "required": [ "ID", "AfterID" ], - "additionalProperties": false - }, - { - "properties": { - "ID": {}, - "ToPosition": { - "title": "To position", - "description": "Move entry so it's right at this position", - "enum": [ "Top", "Bottom" ] - } - }, - "required": [ "ID", "ToPosition" ], - "additionalProperties": false - } - ], - "required": [ "ID" ] - } - } - } - }, - "EditMapChange": { - "properties": { - "FromFile": { - "description": "The relative path to the map in your content pack folder from which to copy (like assets/town.tbin). This can be a .tbin or .xnb file. This field supports tokens and capitalisation doesn't matter.\nContent Patcher will handle tilesheets referenced by the FromFile map for you if it's a .tbin file:\n - If a tilesheet isn't referenced by the target map, Content Patcher will add it for you (with a z_ ID prefix to avoid conflicts with hardcoded game logic). If the source map has a custom version of a tilesheet that's already referenced, it'll be added as a separate tilesheet only used by your tiles.\n - If you include the tilesheet file in your mod folder, Content Patcher will use that one automatically; otherwise it will be loaded from the game's Content/Maps folder.", - "$ref": "#/definitions/FromFile" - }, - "FromArea": { - "title": "Source area", - "description": "The part of the source map to copy. Defaults to the whole source map.", - "$ref": "#/definitions/Rectangle" - }, - "ToArea": { - "title": "Target area", - "description": "The part of the target map to replace.", - "$ref": "#/definitions/Rectangle" - } - }, - "required": [ "FromFile", "ToArea" ] - }, - "Config": { + "ConfigSchema": { + "title": "Config schema", + "description": "Defines the config.json format, to support more complex mods.", "type": "object", "additionalProperties": { "type": "object", @@ -247,9 +43,7 @@ }, "if": { "properties": { - "AllowBlank": { - "const": false - } + "AllowBlank": { "const": false } }, "required": [ "AllowBlank" ] }, @@ -258,33 +52,240 @@ } } }, + "DynamicTokens": { + "title": "Dynamic tokens", + "description": "Custom tokens that you can use.", + "type": "array", + "items": { + "type": "object", + "properties": { + "Name": { + "title": "Name", + "description": "The name of the token to use for tokens & condition.", + "type": "string" + }, + "Value": { + "title": "Token value", + "description": "The value(s) to set. This can be a comma-delimited value to give it multiple values. If any block for a token name has multiple values, it will only be usable in conditions. This field supports tokens, including dynamic tokens defined before this entry.", + "type": "string" + }, + "When": { + "title": "When", + "description": "Only set the value if the given conditions match. If not specified, always matches.", + "$ref": "#/definitions/Condition" + } + }, + "required": [ "Name", "Value" ] + } + }, + "Changes": { + "title": "Changes", + "description": "The changes you want to make. Each entry is called a patch, and describes a specific action to perform: replace this file, copy this image into the file, etc. You can list any number of patches.", + "type": "array", + "items": { + "properties": { + "Action": { + "title": "Action", + "description": "The kind of change to make.", + "type": "string", + "enum": [ "Load", "EditImage", "EditData", "EditMap" ] + }, + "Target": { + "title": "Target asset", + "description": "The game asset you want to patch (or multiple comma-delimited assets). This is the file path inside your game's Content folder, without the file extension or language (like Animals/Dinosaur to edit Content/Animals/Dinosaur.xnb). This field supports tokens and capitalisation doesn't matter. Your changes are applied in all languages unless you specify a language condition.", + "type": "string" + }, + "LogName": { + "title": "Patch log name", + "description": "A name for this patch shown in log messages. This is very useful for understanding errors; if not specified, will default to a name like entry #14 (EditImage Animals/Dinosaurs).", + "type": "string" + }, + "Enabled": { + "title": "Enabled", + "description": "Whether to apply this patch. Default true. This fields supports immutable tokens (e.g. config tokens) if they return true/false.", + "anyOf": [ + { + "type": "string", + "enum": [ "true", "false" ] + }, + { + "type": "string", + "pattern": "\\{\\{[^{}]+\\}\\}" + } + ] + }, + "When": { + "title": "When", + "description": "Only apply the patch if the given conditions match.", + "$ref": "#/definitions/Condition" + } + }, + "required": [ "Action", "Target" ], + "allOf": [ + { + "if": { + "properties": { + "Action": { "const": "Load" } + } + }, + "then": { + "properties": { + "FromFile": { + "$ref": "#/definitions/FromFile" + } + }, + "required": [ "FromFile" ] + } + }, + { + "if": { + "properties": { + "Action": { "const": "EditImage" } + } + }, + "then": { + "properties": { + "FromFile": { + "$ref": "#/definitions/FromFile" + }, + "FromArea": { + "title": "Source area", + "description": "The part of the source image to copy. Defaults to the whole source image.", + "$ref": "#/definitions/Rectangle" + }, + "ToArea": { + "title": "Destination area", + "description": "The part of the target image to replace. Defaults to the FromArea size starting from the top-left corner.", + "$ref": "#/definitions/Rectangle" + }, + "PatchMode": { + "title": "Patch mode", + "description": "How to apply FromArea to ToArea. Defaults to Replace.", + "type": "string", + "enum": [ "Replace", "Overlay" ] + } + }, + "required": [ "FromFile" ] + } + }, + { + "if": { + "properties": { + "Action": { "const": "EditData" } + } + }, + "then": { + "properties": { + "Fields": { + "title": "Fields", + "description": "The individual fields you want to change for existing entries. This field supports tokens in field keys and values. The key for each field is the field index (starting at zero) for a slash-delimited string, or the field name for an object.", + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "Entries": { + "title": "Entries", + "description": "The entries in the data file you want to add, replace, or delete. If you only want to change a few fields, use Fields instead for best compatibility with other mods. To add an entry, just specify a key that doesn't exist; to delete an entry, set the value to null (like \"some key\": null). This field supports tokens in entry keys and values.\nCaution: some XNB files have extra fields at the end for translations; when adding or replacing an entry for all locales, make sure you include the extra fields to avoid errors for non-English players." + }, + "MoveEntries": { + "type": "array", + "items": { + "type": "object", + "properties": { + "ID": { + "title": "ID", + "description": "The ID of the entry to move", + "type": "string" + } + }, + "anyOf": [ + { + "properties": { + "ID": {}, + "BeforeID": { + "title": "Before ID", + "description": "Move entry so it's right before this ID", + "type": "string" + } + }, + "required": [ "ID", "BeforeID" ], + "additionalProperties": false + }, + { + "properties": { + "ID": {}, + "AfterID": { + "title": "After ID", + "description": "Move entry so it's right after this ID", + "type": "string" + } + }, + "required": [ "ID", "AfterID" ], + "additionalProperties": false + }, + { + "properties": { + "ID": {}, + "ToPosition": { + "title": "To position", + "description": "Move entry so it's right at this position", + "enum": [ "Top", "Bottom" ] + } + }, + "required": [ "ID", "ToPosition" ], + "additionalProperties": false + } + ], + "required": [ "ID" ] + } + } + } + } + }, + { + "if": { + "properties": { + "Action": { "const": "EditMap" } + } + }, + "then": { + "properties": { + "FromFile": { + "description": "The relative path to the map in your content pack folder from which to copy (like assets/town.tbin). This can be a .tbin or .xnb file. This field supports tokens and capitalisation doesn't matter.\nContent Patcher will handle tilesheets referenced by the FromFile map for you if it's a .tbin file:\n - If a tilesheet isn't referenced by the target map, Content Patcher will add it for you (with a z_ ID prefix to avoid conflicts with hardcoded game logic). If the source map has a custom version of a tilesheet that's already referenced, it'll be added as a separate tilesheet only used by your tiles.\n - If you include the tilesheet file in your mod folder, Content Patcher will use that one automatically; otherwise it will be loaded from the game's Content/Maps folder.", + "$ref": "#/definitions/FromFile" + }, + "FromArea": { + "title": "Source area", + "description": "The part of the source map to copy. Defaults to the whole source map.", + "$ref": "#/definitions/Rectangle" + }, + "ToArea": { + "title": "Target area", + "description": "The part of the target map to replace.", + "$ref": "#/definitions/Rectangle" + } + }, + "required": [ "FromFile", "ToArea" ] + } + } + ] + } + }, + "$schema": { + "title": "Schema", + "description": "The schema this JSON should follow. Useful for JSON validation tools.", + "type": "string", + "format": "uri" + } + }, + "definitions": { "Condition": { "type": "object", "additionalProperties": { "type": "string" } }, - "DynamicToken": { - "type": "object", - "properties": { - "Name": { - "title": "Name", - "description": "The name of the token to use for tokens & condition.", - "type": "string" - }, - "Value": { - "title": "Token value", - "description": "The value(s) to set. This can be a comma-delimited value to give it multiple values. If any block for a token name has multiple values, it will only be usable in conditions. This field supports tokens, including dynamic tokens defined before this entry.", - "type": "string" - }, - "When": { - "title": "When", - "description": "Only set the value if the given conditions match. If not specified, always matches.", - "$ref": "#/definitions/Condition" - } - }, - "required": [ "Name", "Value" ] - }, "FromFile": { "title": "Source file", "description": "The relative file path in your content pack folder to load instead (like assets/dinosaur.png). This can be a .json (data), .png (image), .tbin (map), or .xnb file. This field supports tokens and capitalisation doesn't matter.", @@ -321,39 +322,6 @@ } } }, - "properties": { - "Format": { - "title": "Format version", - "description": "The format version. You should always use the latest version to use the latest features and avoid obsolete behavior.", - "$ref": "#/definitions/Version" - }, - "Changes": { - "title": "Changes", - "description": "The changes you want to make. Each entry is called a patch, and describes a specific action to perform: replace this file, copy this image into the file, etc. You can list any number of patches.", - "type": "array", - "items": { - "$ref": "#/definitions/Change" - } - }, - "ConfigSchema": { - "title": "Config schema", - "description": "Defines the config.json format, to support more complex mods.", - "$ref": "#/definitions/Config" - }, - "DynamicTokens": { - "title": "Dynamic tokens", - "description": "Custom tokens that you can use.", - "type": "array", - "items": { - "$ref": "#/definitions/DynamicToken" - } - }, - "$schema": { - "title": "Schema", - "description": "The schema this JSON should follow. Useful for JSON validation tools.", - "type": "string", - "format": "uri" - } - }, + "required": [ "Format", "Changes" ] } -- cgit From e51638948f4355c27d6b3f02d637d4ed754ccb73 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 6 Aug 2019 00:52:47 -0400 Subject: add support for @value token in custom schema errors (#654) --- docs/technical/web.md | 2 + .../Controllers/JsonValidatorController.cs | 64 ++++++++++++++-------- 2 files changed, 43 insertions(+), 23 deletions(-) (limited to 'src') diff --git a/docs/technical/web.md b/docs/technical/web.md index a008fe72..108e3671 100644 --- a/docs/technical/web.md +++ b/docs/technical/web.md @@ -44,6 +44,8 @@ format, with some special properties: "oneOf:valid against more than one schema": "Can't specify both EntryDll or ContentPackFor, they're mutually exclusive." } ``` + Error messages can optionally include a `@value` token, which will be replaced with the error's + value field (which is usually the original field value). You can also reference these schemas in your JSON file directly using the `$schema` field, for text editors that support schema validation. For example: diff --git a/src/SMAPI.Web/Controllers/JsonValidatorController.cs b/src/SMAPI.Web/Controllers/JsonValidatorController.cs index 9ded9c0d..cc2a7add 100644 --- a/src/SMAPI.Web/Controllers/JsonValidatorController.cs +++ b/src/SMAPI.Web/Controllers/JsonValidatorController.cs @@ -181,7 +181,7 @@ namespace StardewModdingAPI.Web.Controllers private string GetFlattenedError(ValidationError error, int indent = 0) { // get override error - string message = this.GetOverrideError(error.Schema, error.ErrorType, error.Message); + string message = this.GetOverrideError(error); if (message != null) return message; @@ -235,33 +235,37 @@ namespace StardewModdingAPI.Web.Controllers } /// Get an override error from the JSON schema, if any. - /// The schema or subschema that raised the error. - /// The error type. - /// The error message. - private string GetOverrideError(JSchema schema, ErrorType errorType, string message) + /// The schema validation error. + private string GetOverrideError(ValidationError error) { - // get override errors - IDictionary errors = this.GetExtensionField>(schema, "@errorMessages"); - if (errors == null) - return null; - errors = new Dictionary(errors, StringComparer.InvariantCultureIgnoreCase); - - // match error by type and message - foreach (var pair in errors) + string GetRawOverrideError() { - if (!pair.Key.Contains(":")) - continue; + // get override errors + IDictionary errors = this.GetExtensionField>(error.Schema, "@errorMessages"); + if (errors == null) + return null; + errors = new Dictionary(errors, StringComparer.InvariantCultureIgnoreCase); + + // match error by type and message + foreach (var pair in errors) + { + if (!pair.Key.Contains(":")) + continue; - string[] parts = pair.Key.Split(':', 2); - if (parts[0].Equals(errorType.ToString(), StringComparison.InvariantCultureIgnoreCase) && Regex.IsMatch(message, parts[1])) - return pair.Value; - } + string[] parts = pair.Key.Split(':', 2); + if (parts[0].Equals(error.ErrorType.ToString(), StringComparison.InvariantCultureIgnoreCase) && Regex.IsMatch(error.Message, parts[1])) + return pair.Value; + } - // match by type - if (errors.TryGetValue(errorType.ToString(), out string error)) - return error; + // match by type + if (errors.TryGetValue(error.ErrorType.ToString(), out string message)) + return message; - return null; + return null; + } + + return GetRawOverrideError() + ?.Replace("@value", this.FormatValue(error.Value)); } /// Get an extension field from a JSON schema. @@ -281,5 +285,19 @@ namespace StardewModdingAPI.Web.Controllers return default; } + + /// Format a schema value for display. + /// The value to format. + private string FormatValue(object value) + { + switch (value) + { + case List list: + return string.Join(", ", list); + + default: + return value?.ToString() ?? "null"; + } + } } } -- cgit From d88d3505ec75c3633cdd5d57058a5d290e558dfb Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 6 Aug 2019 01:16:55 -0400 Subject: add error type code to HTML for convenience when adding custom error messages (#654) --- src/SMAPI.Web/Controllers/JsonValidatorController.cs | 4 ++-- .../ViewModels/JsonValidator/JsonValidatorErrorModel.cs | 9 ++++++++- src/SMAPI.Web/Views/JsonValidator/Index.cshtml | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Web/Controllers/JsonValidatorController.cs b/src/SMAPI.Web/Controllers/JsonValidatorController.cs index cc2a7add..c23103dd 100644 --- a/src/SMAPI.Web/Controllers/JsonValidatorController.cs +++ b/src/SMAPI.Web/Controllers/JsonValidatorController.cs @@ -99,7 +99,7 @@ namespace StardewModdingAPI.Web.Controllers } catch (JsonReaderException ex) { - return this.View("Index", result.AddErrors(new JsonValidatorErrorModel(ex.LineNumber, ex.Path, ex.Message))); + return this.View("Index", result.AddErrors(new JsonValidatorErrorModel(ex.LineNumber, ex.Path, ex.Message, ErrorType.None))); } // format JSON @@ -124,7 +124,7 @@ namespace StardewModdingAPI.Web.Controllers // validate JSON parsed.IsValid(schema, out IList rawErrors); var errors = rawErrors - .Select(error => new JsonValidatorErrorModel(error.LineNumber, error.Path, this.GetFlattenedError(error))) + .Select(error => new JsonValidatorErrorModel(error.LineNumber, error.Path, this.GetFlattenedError(error), error.ErrorType)) .ToArray(); return this.View("Index", result.AddErrors(errors)); } diff --git a/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorErrorModel.cs b/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorErrorModel.cs index f9497a38..62b95501 100644 --- a/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorErrorModel.cs +++ b/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorErrorModel.cs @@ -1,3 +1,5 @@ +using Newtonsoft.Json.Schema; + namespace StardewModdingAPI.Web.ViewModels.JsonValidator { /// The view model for a JSON validator error. @@ -15,6 +17,9 @@ namespace StardewModdingAPI.Web.ViewModels.JsonValidator /// A human-readable description of the error. public string Message { get; set; } + /// The schema error type. + public ErrorType SchemaErrorType { get; set; } + /********* ** Public methods @@ -26,11 +31,13 @@ namespace StardewModdingAPI.Web.ViewModels.JsonValidator /// The line number on which the error occurred. /// The field path in the JSON file where the error occurred. /// A human-readable description of the error. - public JsonValidatorErrorModel(int line, string path, string message) + /// The schema error type. + public JsonValidatorErrorModel(int line, string path, string message, ErrorType schemaErrorType) { this.Line = line; this.Path = path; this.Message = message; + this.SchemaErrorType = schemaErrorType; } } } diff --git a/src/SMAPI.Web/Views/JsonValidator/Index.cshtml b/src/SMAPI.Web/Views/JsonValidator/Index.cshtml index 1cf35e39..6e5da7e5 100644 --- a/src/SMAPI.Web/Views/JsonValidator/Index.cshtml +++ b/src/SMAPI.Web/Views/JsonValidator/Index.cshtml @@ -111,7 +111,7 @@ else if (Model.PasteID != null) @foreach (JsonValidatorErrorModel error in Model.Errors) { -
    + -- cgit From 2cc183b48ba33488f3f475a0ab4d931ee6cf2d58 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 6 Aug 2019 01:18:28 -0400 Subject: tweak manifest schema to allow $schema field, fix parse errors in some text editors (#654) --- src/SMAPI.Web/wwwroot/schemas/manifest.json | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Web/wwwroot/schemas/manifest.json b/src/SMAPI.Web/wwwroot/schemas/manifest.json index d4727eda..685b515b 100644 --- a/src/SMAPI.Web/wwwroot/schemas/manifest.json +++ b/src/SMAPI.Web/wwwroot/schemas/manifest.json @@ -102,14 +102,21 @@ "pattern": "^(Chucklefish:\\d+|Nexus:\\d+|GitHub:[A-Za-z0-9_]+/[A-Za-z0-9_]+|ModDrop:\\d+)$", "@errorMessages": { "pattern": "Invalid update key; see https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest#Update_checks for more info." - } + } } + }, + "$schema": { + "title": "Schema", + "description": "A reference to this JSON schema. Not part of the actual format, but useful for validation tools.", + "type": "string", + "const": "https://smapi.io/schemas/manifest.json" } }, "definitions": { "SemanticVersion": { "type": "string", - "pattern": "^(?>(?0|[1-9]\\d*))\\.(?>(?0|[1-9]\\d*))(?>(?:\\.(?0|[1-9]\\d*))?)(?:-(?(?>[a-zA-Z0-9]+[\\-\\.]?)+))?$", // derived from SMAPI.Toolkit.SemanticVersion + "pattern": "^(?>(?:0|[1-9]\\d*))\\.(?>(?:0|[1-9]\\d*))(?>(?:\\.(?:0|[1-9]\\d*))?)(?:-(?:(?>[a-zA-Z0-9]+[\\-\\.]?)+))?$", + "$comment": "derived from SMAPI.Toolkit.SemanticVersion", "examples": [ "1.0.0", "1.0.1-beta.2" ], "@errorMessages": { "pattern": "Invalid semantic version; must be formatted like 1.2.0 or 1.2.0-prerelease.tags. See https://semver.org/ for more info." @@ -117,7 +124,8 @@ }, "ModID": { "type": "string", - "pattern": "^[a-zA-Z0-9_.-]+$", // derived from SMAPI.Toolkit.Utilities.PathUtilities.IsSlug + "pattern": "^[a-zA-Z0-9_.-]+$", + "$comment": "derived from SMAPI.Toolkit.Utilities.PathUtilities.IsSlug", "examples": [ "Pathoschild.LookupAnything" ] } }, -- cgit From 74e86de01e0f3fc7424a9dd0e7fa43853b35ae1e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 6 Aug 2019 01:31:15 -0400 Subject: when uploading a new file to the JSON validator, prefill previous schema type by default (#654) --- src/SMAPI.Web/Views/JsonValidator/Index.cshtml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Web/Views/JsonValidator/Index.cshtml b/src/SMAPI.Web/Views/JsonValidator/Index.cshtml index 6e5da7e5..34c1c1f3 100644 --- a/src/SMAPI.Web/Views/JsonValidator/Index.cshtml +++ b/src/SMAPI.Web/Views/JsonValidator/Index.cshtml @@ -3,6 +3,11 @@ @{ ViewData["Title"] = "JSON validator"; + + string curPageUrl = new Uri(new Uri(Model.SectionUrl), $"{Model.SchemaName}/{Model.PasteID}").ToString(); + string newUploadUrl = Model.SchemaName != null + ? new Uri(new Uri(Model.SectionUrl), Model.SchemaName).ToString() + : Model.SectionUrl; } @section Head { @@ -37,8 +42,8 @@ else if (Model.ParseError != null) { @@ -46,8 +51,8 @@ else if (Model.ParseError != null) else if (Model.PasteID != null) { } -- cgit From 674ceea74e74c5b0f432534dba981b5066ea7630 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 6 Aug 2019 03:19:38 -0400 Subject: add support for transparent schema errors (#654) --- docs/release-notes.md | 3 +- docs/technical/web.md | 101 +++++++++++++-------- .../Controllers/JsonValidatorController.cs | 89 +++++++++++------- 3 files changed, 124 insertions(+), 69 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 9ce88db1..e253c3c0 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -39,7 +39,8 @@ These changes have not been released yet. * For the JSON validator: * Added JSON validator at [json.smapi.io](https://json.smapi.io), which lets you validate a JSON file against predefined mod formats. * Added support for the `manifest.json` format. - * Added support for Content Patcher's `content.json` format (thanks to TehPers!). + * Added support for the Content Patcher format (thanks to TehPers!). + * Added support for referencing a schema in a JSON Schema-compatible text editor. * For modders: * Mods are now loaded much earlier in the game launch. This lets mods intercept any content asset, but the game is not fully initialised when `Entry` is called (use the `GameLaunched` event if you need to run code when the game is initialised). diff --git a/docs/technical/web.md b/docs/technical/web.md index 108e3671..91af2f98 100644 --- a/docs/technical/web.md +++ b/docs/technical/web.md @@ -4,50 +4,77 @@ and update check API. ## Contents -* [Overview](#overview) - * [Log parser](#log-parser) - * [JSON validator](#json-validator) - * [Web API](#web-api) +* [Log parser](#log-parser) +* [JSON validator](#json-validator) +* [Web API](#web-api) * [For SMAPI developers](#for-smapi-developers) * [Local development](#local-development) * [Deploying to Amazon Beanstalk](#deploying-to-amazon-beanstalk) -## Overview -The `SMAPI.Web` project provides an API and web UI hosted at `*.smapi.io`. - -### Log parser +## Log parser The log parser provides a web UI for uploading, parsing, and sharing SMAPI logs. The logs are persisted in a compressed form to Pastebin. The log parser lives at https://log.smapi.io. -### JSON validator -The JSON validator provides a web UI for uploading and sharing JSON files, and validating them -as plain JSON or against a predefined format like `manifest.json` or Content Patcher's -`content.json`. The JSON validator lives at https://json.smapi.io. +## JSON validator +### Overview +The JSON validator provides a web UI for uploading and sharing JSON files, and validating them as +plain JSON or against a predefined format like `manifest.json` or Content Patcher's `content.json`. +The JSON validator lives at https://json.smapi.io. +### Schema file format Schema files are defined in `wwwroot/schemas` using the [JSON Schema](https://json-schema.org/) -format, with some special properties: -* The root schema may have a `@documentationURL` field, which is the URL to the user-facing - documentation for the format (if any). -* Any part of the schema can define an `@errorMessages` field, which specifies user-friendly errors - which override the auto-generated messages. These can be indexed by error type: - ```js - "pattern": "^[a-zA-Z0-9_.-]+\\.dll$", - "@errorMessages": { - "pattern": "Invalid value; must be a filename ending with .dll." - } - ``` - ...or by error type and a regular expression applied to the default message (not recommended - unless the previous form doesn't work, since it's more likely to break in future versions): - ```js - "@errorMessages": { - "oneOf:valid against no schemas": "Missing required field: EntryDll or ContentPackFor.", - "oneOf:valid against more than one schema": "Can't specify both EntryDll or ContentPackFor, they're mutually exclusive." - } - ``` - Error messages can optionally include a `@value` token, which will be replaced with the error's - value field (which is usually the original field value). - -You can also reference these schemas in your JSON file directly using the `$schema` field, for +format. The JSON validator UI recognises a superset of the standard fields to change output: + +
    +
    Documentation URL
    +
    + +The root schema may have a `@documentationURL` field, which is a web URL for the user +documentation: +```js +"@documentationUrl": "https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest" +``` + +If present, this is shown in the JSON validator UI. + +
    +
    Error messages
    +
    + +Any part of the schema can define an `@errorMessages` field, which overrides matching schema +errors. You can override by error code (recommended), or by error type and a regex pattern matched +against the error message (more fragile): + +```js +// by error type +"pattern": "^[a-zA-Z0-9_.-]+\\.dll$", +"@errorMessages": { + "pattern": "Invalid value; must be a filename ending with .dll." +} +``` +```js +// by error type + message pattern +"@errorMessages": { + "oneOf:valid against no schemas": "Missing required field: EntryDll or ContentPackFor.", + "oneOf:valid against more than one schema": "Can't specify both EntryDll or ContentPackFor, they're mutually exclusive." +} +``` + +Error messages may contain special tokens: +* `@value` is replaced with the error's value field (which is usually the original field value, but + not always). +* If the validation error has exactly one sub-error and the message is set to `$transparent`, the + sub-error will be displayed instead. (The sub-error itself may be set to `$transparent`, etc.) + +Caveats: +* To override an error from a `then` block, the `@errorMessages` must be inside the `then` block + instead of adjacent. + +
    +
    + +### Using a schema file directly +You can reference the validator schemas in your JSON file directly using the `$schema` field, for text editors that support schema validation. For example: ```js { @@ -64,11 +91,13 @@ format | schema URL [SMAPI `manifest.json`](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest) | https://smapi.io/schemas/manifest.json [Content Patcher `content.json`](https://github.com/Pathoschild/StardewMods/tree/develop/ContentPatcher#readme) | https://smapi.io/schemas/content-patcher.json -### Web API +## Web API +### Overview SMAPI provides a web API at `api.smapi.io` for use by SMAPI and external tools. The URL includes a `{version}` token, which is the SMAPI version for backwards compatibility. This API is publicly accessible but not officially released; it may change at any time. +### `/mods` endpoint The API has one `/mods` endpoint. This provides mod info, including official versions and URLs (from Chucklefish, GitHub, or Nexus), unofficial versions from the wiki, and optional mod metadata from the wiki and SMAPI's internal data. This is used by SMAPI to perform update checks, and by diff --git a/src/SMAPI.Web/Controllers/JsonValidatorController.cs b/src/SMAPI.Web/Controllers/JsonValidatorController.cs index c23103dd..cd0a6439 100644 --- a/src/SMAPI.Web/Controllers/JsonValidatorController.cs +++ b/src/SMAPI.Web/Controllers/JsonValidatorController.cs @@ -124,7 +124,7 @@ namespace StardewModdingAPI.Web.Controllers // validate JSON parsed.IsValid(schema, out IList rawErrors); var errors = rawErrors - .Select(error => new JsonValidatorErrorModel(error.LineNumber, error.Path, this.GetFlattenedError(error), error.ErrorType)) + .Select(this.GetErrorModel) .ToArray(); return this.View("Index", result.AddErrors(errors)); } @@ -175,35 +175,6 @@ namespace StardewModdingAPI.Web.Controllers return response; } - /// Get a flattened, human-readable message representing a schema validation error. - /// The error to represent. - /// The indentation level to apply for inner errors. - private string GetFlattenedError(ValidationError error, int indent = 0) - { - // get override error - string message = this.GetOverrideError(error); - if (message != null) - return message; - - // get friendly representation of main error - message = error.Message; - switch (error.ErrorType) - { - case ErrorType.Enum: - message = $"Invalid value. Found '{error.Value}', but expected one of '{string.Join("', '", error.Schema.Enum)}'."; - break; - - case ErrorType.Required: - message = $"Missing required fields: {string.Join(", ", (List)error.Value)}."; - break; - } - - // add inner errors - foreach (ValidationError childError in error.ChildErrors) - message += "\n" + "".PadLeft(indent * 2, ' ') + $"==> {childError.Path}: " + this.GetFlattenedError(childError, indent + 1); - return message; - } - /// Get a normalised schema name, or the if blank. /// The raw schema name to normalise. private string NormaliseSchemaName(string schemaName) @@ -234,6 +205,60 @@ namespace StardewModdingAPI.Web.Controllers return null; } + /// Get a flattened representation representation of a schema validation error and any child errors. + /// The error to represent. + private JsonValidatorErrorModel GetErrorModel(ValidationError error) + { + // skip through transparent errors + while (this.GetOverrideError(error) == "$transparent" && error.ChildErrors.Count == 1) + error = error.ChildErrors[0]; + + // get message + string message = this.GetOverrideError(error); + if (message == null) + message = this.FlattenErrorMessage(error); + + // build model + return new JsonValidatorErrorModel(error.LineNumber, error.Path, message, error.ErrorType); + } + + /// Get a flattened, human-readable message for a schema validation error and any child errors. + /// The error to represent. + /// The indentation level to apply for inner errors. + private string FlattenErrorMessage(ValidationError error, int indent = 0) + { + // get override + string message = this.GetOverrideError(error); + if (message != null) + return message; + + // skip through transparent errors + while (this.GetOverrideError(error) == "$transparent" && error.ChildErrors.Count == 1) + error = error.ChildErrors[0]; + + // get friendly representation of main error + message = error.Message; + switch (error.ErrorType) + { + case ErrorType.Const: + message = $"Invalid value. Found '{error.Value}', but expected '{error.Schema.Const}'."; + break; + + case ErrorType.Enum: + message = $"Invalid value. Found '{error.Value}', but expected one of '{string.Join("', '", error.Schema.Enum)}'."; + break; + + case ErrorType.Required: + message = $"Missing required fields: {string.Join(", ", (List)error.Value)}."; + break; + } + + // add inner errors + foreach (ValidationError childError in error.ChildErrors) + message += "\n" + "".PadLeft(indent * 2, ' ') + $"==> {childError.Path}: " + this.FlattenErrorMessage(childError, indent + 1); + return message; + } + /// Get an override error from the JSON schema, if any. /// The schema validation error. private string GetOverrideError(ValidationError error) @@ -254,12 +279,12 @@ namespace StardewModdingAPI.Web.Controllers string[] parts = pair.Key.Split(':', 2); if (parts[0].Equals(error.ErrorType.ToString(), StringComparison.InvariantCultureIgnoreCase) && Regex.IsMatch(error.Message, parts[1])) - return pair.Value; + return pair.Value?.Trim(); } // match by type if (errors.TryGetValue(error.ErrorType.ToString(), out string message)) - return message; + return message?.Trim(); return null; } -- cgit From 807868f4404d850c29ad8ba4cd69beb9f08cecc6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 6 Aug 2019 03:52:32 -0400 Subject: add support for transparent schema errors with multiple child errors (#654) --- docs/technical/web.md | 27 ++++++++++++++++++---- .../Controllers/JsonValidatorController.cs | 25 ++++++++++++-------- 2 files changed, 39 insertions(+), 13 deletions(-) (limited to 'src') diff --git a/docs/technical/web.md b/docs/technical/web.md index 91af2f98..c13e24e9 100644 --- a/docs/technical/web.md +++ b/docs/technical/web.md @@ -61,10 +61,29 @@ against the error message (more fragile): ``` Error messages may contain special tokens: -* `@value` is replaced with the error's value field (which is usually the original field value, but - not always). -* If the validation error has exactly one sub-error and the message is set to `$transparent`, the - sub-error will be displayed instead. (The sub-error itself may be set to `$transparent`, etc.) + +* The `@value` token is replaced with the error's value field. This is usually (but not always) the + original field value. +* When an error has child errors, by default they're flattened into one message: + ``` + line | field | error + ---- | ---------- | ------------------------------------------------------------------------- + 4 | Changes[0] | JSON does not match schema from 'then'. + | | ==> Changes[0].ToArea.Y: Invalid type. Expected Integer but got String. + | | ==> Changes[0].ToArea: Missing required fields: Height. + ``` + + If you set the message for an error to `$transparent`, the parent error is omitted entirely and + the child errors are shown instead: + ``` + line | field | error + ---- | ------------------- | ---------------------------------------------- + 8 | Changes[0].ToArea.Y | Invalid type. Expected Integer but got String. + 8 | Changes[0].ToArea | Missing required fields: Height. + ``` + + The child errors themselves may be marked `$transparent`, etc. If an error has no child errors, + this override is ignored. Caveats: * To override an error from a `then` block, the `@errorMessages` must be inside the `then` block diff --git a/src/SMAPI.Web/Controllers/JsonValidatorController.cs b/src/SMAPI.Web/Controllers/JsonValidatorController.cs index cd0a6439..4f234449 100644 --- a/src/SMAPI.Web/Controllers/JsonValidatorController.cs +++ b/src/SMAPI.Web/Controllers/JsonValidatorController.cs @@ -46,6 +46,9 @@ namespace StardewModdingAPI.Web.Controllers /// The schema ID to use if none was specified. private string DefaultSchemaID = "manifest"; + /// A token in an error message which indicates that the child errors should be displayed instead. + private readonly string TransparentToken = "$transparent"; + /********* ** Public methods @@ -124,7 +127,7 @@ namespace StardewModdingAPI.Web.Controllers // validate JSON parsed.IsValid(schema, out IList rawErrors); var errors = rawErrors - .Select(this.GetErrorModel) + .SelectMany(this.GetErrorModels) .ToArray(); return this.View("Index", result.AddErrors(errors)); } @@ -205,21 +208,25 @@ namespace StardewModdingAPI.Web.Controllers return null; } - /// Get a flattened representation representation of a schema validation error and any child errors. + /// Get view models representing a schema validation error and any child errors. /// The error to represent. - private JsonValidatorErrorModel GetErrorModel(ValidationError error) + private IEnumerable GetErrorModels(ValidationError error) { // skip through transparent errors - while (this.GetOverrideError(error) == "$transparent" && error.ChildErrors.Count == 1) - error = error.ChildErrors[0]; + if (this.GetOverrideError(error) == this.TransparentToken && error.ChildErrors.Any()) + { + foreach (var model in error.ChildErrors.SelectMany(this.GetErrorModels)) + yield return model; + yield break; + } // get message string message = this.GetOverrideError(error); - if (message == null) + if (message == null || message == this.TransparentToken) message = this.FlattenErrorMessage(error); // build model - return new JsonValidatorErrorModel(error.LineNumber, error.Path, message, error.ErrorType); + yield return new JsonValidatorErrorModel(error.LineNumber, error.Path, message, error.ErrorType); } /// Get a flattened, human-readable message for a schema validation error and any child errors. @@ -229,11 +236,11 @@ namespace StardewModdingAPI.Web.Controllers { // get override string message = this.GetOverrideError(error); - if (message != null) + if (message != null && message != this.TransparentToken) return message; // skip through transparent errors - while (this.GetOverrideError(error) == "$transparent" && error.ChildErrors.Count == 1) + while (this.GetOverrideError(error) == this.TransparentToken && error.ChildErrors.Count == 1) error = error.ChildErrors[0]; // get friendly representation of main error -- cgit From 5679df8d66c7ec5f5894761a4f9950a0d2ee9287 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 6 Aug 2019 05:07:15 -0400 Subject: expand Content Patcher schema, detect more common issues (#654) --- src/SMAPI.Web/wwwroot/schemas/content-patcher.json | 336 +++++++++++++-------- 1 file changed, 202 insertions(+), 134 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json index 25cba70e..c7f882bc 100644 --- a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json +++ b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json @@ -9,9 +9,12 @@ "properties": { "Format": { "title": "Format version", - "description": "The format version. You should always use the latest version to use the latest features and avoid obsolete behavior.", + "description": "The format version. You should always use the latest version to enable the latest features and avoid obsolete behavior.", "type": "string", - "pattern": "\\d+\\.\\d+" + "const": "1.9", + "@errorMessages": { + "const": "Incorrect value '@value'. This should be set to the latest format version, currently '1.9'." + } }, "ConfigSchema": { "title": "Config schema", @@ -39,16 +42,26 @@ "title": "Default value", "description": "The default values when the field is missing. Can contain multiple comma-delimited values if AllowMultiple is true. If omitted, blank fields are left blank.", "type": "string" - } - }, - "if": { - "properties": { - "AllowBlank": { "const": false } }, - "required": [ "AllowBlank" ] + + "additionalProperties": false }, - "then": { - "required": [ "Default" ] + "allOf": [ + { + "if": { + "properties": { + "AllowBlank": { "const": false } + }, + "required": [ "AllowBlank" ] + }, + "then": { + "required": [ "Default" ] + } + } + ], + + "@errorMessages": { + "allOf": "If 'AllowBlank' is false, the 'Default' field is required." } } }, @@ -61,7 +74,7 @@ "properties": { "Name": { "title": "Name", - "description": "The name of the token to use for tokens & condition.", + "description": "The name of the token to use for tokens & conditions.", "type": "string" }, "Value": { @@ -75,7 +88,9 @@ "$ref": "#/definitions/Condition" } }, - "required": [ "Name", "Value" ] + + "required": [ "Name", "Value" ], + "additionalProperties": false } }, "Changes": { @@ -93,11 +108,17 @@ "Target": { "title": "Target asset", "description": "The game asset you want to patch (or multiple comma-delimited assets). This is the file path inside your game's Content folder, without the file extension or language (like Animals/Dinosaur to edit Content/Animals/Dinosaur.xnb). This field supports tokens and capitalisation doesn't matter. Your changes are applied in all languages unless you specify a language condition.", - "type": "string" + "type": "string", + "not": { + "pattern": "^ *[cC][oO][nN][tT][eE][nN][tT]/|\\.[xX][nN][bB] *$|\\.[a-zA-Z][a-zA-Z]-[a-zA-Z][a-zA-Z](?:.xnb)? *$" + }, + "@errorMessages": { + "not": "Invalid target; it shouldn't include the 'Content/' folder, '.xnb' extension, or locale code." + } }, "LogName": { "title": "Patch log name", - "description": "A name for this patch shown in log messages. This is very useful for understanding errors; if not specified, will default to a name like entry #14 (EditImage Animals/Dinosaurs).", + "description": "A name for this patch shown in log messages. This is very useful for understanding errors; if not specified, will default to a name like 'entry #14 (EditImage Animals/Dinosaurs)'.", "type": "string" }, "Enabled": { @@ -112,7 +133,128 @@ "type": "string", "pattern": "\\{\\{[^{}]+\\}\\}" } - ] + ], + "@errorMessages": { + "anyOf": "Invalid value; must be true, false, or a single token which evaluates to true or false." + } + }, + "FromFile": { + "title": "Source file", + "description": "The relative file path in your content pack folder to load instead (like 'assets/dinosaur.png'). This can be a .json (data), .png (image), .tbin (map), or .xnb file. This field supports tokens and capitalisation doesn't matter.", + "type": "string", + "allOf": [ + { + "not": { + "pattern": "\b\\.\\.[/\\]" + } + }, + { + "pattern": "\\.(json|png|tbin|xnb) *$" + } + ], + "@errorMessages": { + "allOf:indexes: 0": "Invalid value; must not contain directory climbing (like '../').", + "allOf:indexes: 1": "Invalid value; must be a file path ending with .json, .png, .tbin, or .xnb." + } + }, + "FromArea": { + "title": "Source area", + "description": "The part of the source image to copy. Defaults to the whole source image.", + "$ref": "#/definitions/Rectangle" + }, + "ToArea": { + "title": "Destination area", + "description": "The part of the target image to replace. Defaults to the FromArea size starting from the top-left corner.", + "$ref": "#/definitions/Rectangle" + }, + "PatchMode": { + "title": "Patch mode", + "description": "How to apply FromArea to ToArea. Defaults to Replace.", + "type": "string", + "enum": [ "Replace", "Overlay" ], + "default": "Replace" + }, + "Fields": { + "title": "Fields", + "description": "The individual fields you want to change for existing entries. This field supports tokens in field keys and values. The key for each field is the field index (starting at zero) for a slash-delimited string, or the field name for an object.", + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "Entries": { + "title": "Entries", + "description": "The entries in the data file you want to add, replace, or delete. If you only want to change a few fields, use Fields instead for best compatibility with other mods. To add an entry, just specify a key that doesn't exist; to delete an entry, set the value to null (like \"some key\": null). This field supports tokens in entry keys and values.\nCaution: some XNB files have extra fields at the end for translations; when adding or replacing an entry for all locales, make sure you include the extra fields to avoid errors for non-English players.", + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "MoveEntries": { + "type": "array", + "items": { + "type": "object", + "properties": { + "ID": { + "title": "ID", + "description": "The ID of the entry to move", + "type": "string" + }, + "BeforeID": { + "title": "Before ID", + "description": "Move entry so it's right before this ID", + "type": "string" + }, + "AfterID": { + "title": "After ID", + "description": "Move entry so it's right after this ID", + "type": "string" + }, + "ToPosition": { + "title": "To position", + "description": "Move entry so it's right at this position", + "enum": [ "Top", "Bottom" ] + } + }, + + "anyOf": [ + { + "required": [ "BeforeID" ] + }, + { + "required": [ "AfterID" ] + }, + { + "required": [ "ToPosition" ] + } + ], + + "dependencies": { + "BeforeID": { + "propertyNames": { + "enum": [ "ID", "BeforeID" ] + } + }, + "AfterID": { + "propertyNames": { + "enum": [ "ID", "AfterID" ] + } + }, + "ToPosition": { + "propertyNames": { + "enum": [ "ID", "ToPosition" ] + } + } + }, + + "required": [ "ID" ], + "@errorMessages": { + "anyOf": "You must specify one of 'AfterID', 'BeforeID', or 'ToPosition'.", + "dependencies:BeforeID": "If 'BeforeID' is specified, only 'ID' and 'BeforeID' fields are valid.", + "dependencies:AfterID": "If 'AfterID' is specified, only 'ID' and 'AfterID' fields are valid.", + "dependencies:ToPosition": "If 'ToPosition' is specified, only 'ID' and 'ToPosition' fields are valid." + } + } }, "When": { "title": "When", @@ -120,7 +262,6 @@ "$ref": "#/definitions/Condition" } }, - "required": [ "Action", "Target" ], "allOf": [ { "if": { @@ -129,12 +270,13 @@ } }, "then": { - "properties": { - "FromFile": { - "$ref": "#/definitions/FromFile" - } + "required": [ "FromFile" ], + "propertyNames": { + "enum": [ "Action", "Target", "LogName", "Enabled", "When", "FromFile" ] }, - "required": [ "FromFile" ] + "@errorMessages": { + "then": "$transparent" + } } }, { @@ -144,28 +286,13 @@ } }, "then": { - "properties": { - "FromFile": { - "$ref": "#/definitions/FromFile" - }, - "FromArea": { - "title": "Source area", - "description": "The part of the source image to copy. Defaults to the whole source image.", - "$ref": "#/definitions/Rectangle" - }, - "ToArea": { - "title": "Destination area", - "description": "The part of the target image to replace. Defaults to the FromArea size starting from the top-left corner.", - "$ref": "#/definitions/Rectangle" - }, - "PatchMode": { - "title": "Patch mode", - "description": "How to apply FromArea to ToArea. Defaults to Replace.", - "type": "string", - "enum": [ "Replace", "Overlay" ] - } + "required": [ "FromFile" ], + "propertyNames": { + "enum": [ "Action", "Target", "LogName", "Enabled", "When", "FromFile", "FromArea", "ToArea", "PatchMode" ] }, - "required": [ "FromFile" ] + "@errorMessages": { + "then": "$transparent" + } } }, { @@ -175,71 +302,11 @@ } }, "then": { - "properties": { - "Fields": { - "title": "Fields", - "description": "The individual fields you want to change for existing entries. This field supports tokens in field keys and values. The key for each field is the field index (starting at zero) for a slash-delimited string, or the field name for an object.", - "type": "object", - "additionalProperties": { - "type": "object" - } - }, - "Entries": { - "title": "Entries", - "description": "The entries in the data file you want to add, replace, or delete. If you only want to change a few fields, use Fields instead for best compatibility with other mods. To add an entry, just specify a key that doesn't exist; to delete an entry, set the value to null (like \"some key\": null). This field supports tokens in entry keys and values.\nCaution: some XNB files have extra fields at the end for translations; when adding or replacing an entry for all locales, make sure you include the extra fields to avoid errors for non-English players." - }, - "MoveEntries": { - "type": "array", - "items": { - "type": "object", - "properties": { - "ID": { - "title": "ID", - "description": "The ID of the entry to move", - "type": "string" - } - }, - "anyOf": [ - { - "properties": { - "ID": {}, - "BeforeID": { - "title": "Before ID", - "description": "Move entry so it's right before this ID", - "type": "string" - } - }, - "required": [ "ID", "BeforeID" ], - "additionalProperties": false - }, - { - "properties": { - "ID": {}, - "AfterID": { - "title": "After ID", - "description": "Move entry so it's right after this ID", - "type": "string" - } - }, - "required": [ "ID", "AfterID" ], - "additionalProperties": false - }, - { - "properties": { - "ID": {}, - "ToPosition": { - "title": "To position", - "description": "Move entry so it's right at this position", - "enum": [ "Top", "Bottom" ] - } - }, - "required": [ "ID", "ToPosition" ], - "additionalProperties": false - } - ], - "required": [ "ID" ] - } - } + "propertyNames": { + "enum": [ "Action", "Target", "LogName", "Enabled", "When", "Fields", "Entries", "MoveEntries" ] + }, + "@errorMessages": { + "then": "$transparent" } } }, @@ -252,31 +319,34 @@ "then": { "properties": { "FromFile": { - "description": "The relative path to the map in your content pack folder from which to copy (like assets/town.tbin). This can be a .tbin or .xnb file. This field supports tokens and capitalisation doesn't matter.\nContent Patcher will handle tilesheets referenced by the FromFile map for you if it's a .tbin file:\n - If a tilesheet isn't referenced by the target map, Content Patcher will add it for you (with a z_ ID prefix to avoid conflicts with hardcoded game logic). If the source map has a custom version of a tilesheet that's already referenced, it'll be added as a separate tilesheet only used by your tiles.\n - If you include the tilesheet file in your mod folder, Content Patcher will use that one automatically; otherwise it will be loaded from the game's Content/Maps folder.", - "$ref": "#/definitions/FromFile" + "description": "The relative path to the map in your content pack folder from which to copy (like assets/town.tbin). This can be a .tbin or .xnb file. This field supports tokens and capitalisation doesn't matter.\nContent Patcher will handle tilesheets referenced by the FromFile map for you if it's a .tbin file:\n - If a tilesheet isn't referenced by the target map, Content Patcher will add it for you (with a z_ ID prefix to avoid conflicts with hardcoded game logic). If the source map has a custom version of a tilesheet that's already referenced, it'll be added as a separate tilesheet only used by your tiles.\n - If you include the tilesheet file in your mod folder, Content Patcher will use that one automatically; otherwise it will be loaded from the game's Content/Maps folder." }, "FromArea": { - "title": "Source area", - "description": "The part of the source map to copy. Defaults to the whole source map.", - "$ref": "#/definitions/Rectangle" + "description": "The part of the source map to copy. Defaults to the whole source map." }, "ToArea": { - "title": "Target area", - "description": "The part of the target map to replace.", - "$ref": "#/definitions/Rectangle" + "description": "The part of the target map to replace." } }, + "propertyNames": { + "enum": [ "Action", "Target", "LogName", "Enabled", "When", "FromFile", "FromArea", "ToArea" ] + }, "required": [ "FromFile", "ToArea" ] } } - ] + ], + + "required": [ "Action", "Target" ], + "@errorMessages": { + "allOf": "$transparent" + } } }, "$schema": { "title": "Schema", - "description": "The schema this JSON should follow. Useful for JSON validation tools.", + "description": "A reference to this JSON schema. Not part of the actual format, but useful for validation tools.", "type": "string", - "format": "uri" + "const": "https://smapi.io/schemas/content-patcher.json" } }, "definitions": { @@ -286,42 +356,40 @@ "type": "string" } }, - "FromFile": { - "title": "Source file", - "description": "The relative file path in your content pack folder to load instead (like assets/dinosaur.png). This can be a .json (data), .png (image), .tbin (map), or .xnb file. This field supports tokens and capitalisation doesn't matter.", - "type": "string", - "pattern": ".*\\.(json|png|tbin|xnb)$|.*\\{\\{[^}]+}}$" - }, "Rectangle": { "type": "object", "properties": { "X": { "title": "X-Coordinate", "description": "Location in pixels of the top-left of the rectangle", - "type": "number", - "multipleOf": 1 + "type": "integer", + "minimum:": 0 }, "Y": { "title": "Y-Coordinate", "description": "Location in pixels of the top-left of the rectangle", - "type": "number", - "multipleOf": 1 + "type": "integer", + "minimum:": 0 }, "Width": { "title": "Width", "description": "The width of the rectangle", - "type": "number", - "multipleOf": 1 + "type": "integer", + "minimum:": 0 }, "Height": { "title": "Height", "description": "The height of the rectangle", - "type": "number", - "multipleOf": 1 + "type": "integer", + "minimum:": 0 } - } + }, + + "required": [ "X", "Y", "Width", "Height" ], + "additionalProperties": false } }, - "required": [ "Format", "Changes" ] + "required": [ "Format", "Changes" ], + "additionalProperties": false } -- cgit From 6036fbf0500795109b1c5dcf53162cb55834d35a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 6 Aug 2019 05:14:02 -0400 Subject: make 'then' blocks transparent by default (#654) --- docs/technical/web.md | 4 +--- src/SMAPI.Web/Controllers/JsonValidatorController.cs | 17 +++++++++++++++-- src/SMAPI.Web/wwwroot/schemas/content-patcher.json | 9 --------- 3 files changed, 16 insertions(+), 14 deletions(-) (limited to 'src') diff --git a/docs/technical/web.md b/docs/technical/web.md index c13e24e9..27834a4f 100644 --- a/docs/technical/web.md +++ b/docs/technical/web.md @@ -85,9 +85,7 @@ Error messages may contain special tokens: The child errors themselves may be marked `$transparent`, etc. If an error has no child errors, this override is ignored. -Caveats: -* To override an error from a `then` block, the `@errorMessages` must be inside the `then` block - instead of adjacent. + Validation errors for `then` blocks are transparent by default, unless overridden. diff --git a/src/SMAPI.Web/Controllers/JsonValidatorController.cs b/src/SMAPI.Web/Controllers/JsonValidatorController.cs index 4f234449..d82765e7 100644 --- a/src/SMAPI.Web/Controllers/JsonValidatorController.cs +++ b/src/SMAPI.Web/Controllers/JsonValidatorController.cs @@ -213,7 +213,7 @@ namespace StardewModdingAPI.Web.Controllers private IEnumerable GetErrorModels(ValidationError error) { // skip through transparent errors - if (this.GetOverrideError(error) == this.TransparentToken && error.ChildErrors.Any()) + if (this.IsTransparentError(error)) { foreach (var model in error.ChildErrors.SelectMany(this.GetErrorModels)) yield return model; @@ -240,7 +240,7 @@ namespace StardewModdingAPI.Web.Controllers return message; // skip through transparent errors - while (this.GetOverrideError(error) == this.TransparentToken && error.ChildErrors.Count == 1) + if (this.IsTransparentError(error)) error = error.ChildErrors[0]; // get friendly representation of main error @@ -266,6 +266,19 @@ namespace StardewModdingAPI.Web.Controllers return message; } + /// Get whether a validation error should be omitted in favor of its child errors in user-facing error messages. + /// The error to check. + private bool IsTransparentError(ValidationError error) + { + if (!error.ChildErrors.Any()) + return false; + + string @override = this.GetOverrideError(error); + return + @override == this.TransparentToken + || (error.ErrorType == ErrorType.Then && @override == null); + } + /// Get an override error from the JSON schema, if any. /// The schema validation error. private string GetOverrideError(ValidationError error) diff --git a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json index c7f882bc..315a1fb2 100644 --- a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json +++ b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json @@ -273,9 +273,6 @@ "required": [ "FromFile" ], "propertyNames": { "enum": [ "Action", "Target", "LogName", "Enabled", "When", "FromFile" ] - }, - "@errorMessages": { - "then": "$transparent" } } }, @@ -289,9 +286,6 @@ "required": [ "FromFile" ], "propertyNames": { "enum": [ "Action", "Target", "LogName", "Enabled", "When", "FromFile", "FromArea", "ToArea", "PatchMode" ] - }, - "@errorMessages": { - "then": "$transparent" } } }, @@ -304,9 +298,6 @@ "then": { "propertyNames": { "enum": [ "Action", "Target", "LogName", "Enabled", "When", "Fields", "Entries", "MoveEntries" ] - }, - "@errorMessages": { - "then": "$transparent" } } }, -- cgit From 3f6865e8301535c8fbe83bc0f931a116adac0636 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 6 Aug 2019 13:35:20 -0400 Subject: add footer tip about using schema directly, add details to page title (#654) --- docs/technical/web.md | 2 +- src/SMAPI.Web/Views/JsonValidator/Index.cshtml | 27 +++++++++++++++++----- src/SMAPI.Web/Views/Shared/_Layout.cshtml | 2 +- .../wwwroot/Content/css/json-validator.css | 9 ++++++++ 4 files changed, 32 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/docs/technical/web.md b/docs/technical/web.md index 27834a4f..8fd99f82 100644 --- a/docs/technical/web.md +++ b/docs/technical/web.md @@ -101,7 +101,7 @@ text editors that support schema validation. For example: } ``` -Current schemas: +Available schemas: format | schema URL ------ | ---------- diff --git a/src/SMAPI.Web/Views/JsonValidator/Index.cshtml b/src/SMAPI.Web/Views/JsonValidator/Index.cshtml index 34c1c1f3..3143fad9 100644 --- a/src/SMAPI.Web/Views/JsonValidator/Index.cshtml +++ b/src/SMAPI.Web/Views/JsonValidator/Index.cshtml @@ -2,12 +2,22 @@ @model JsonValidatorModel @{ - ViewData["Title"] = "JSON validator"; - + // get view data string curPageUrl = new Uri(new Uri(Model.SectionUrl), $"{Model.SchemaName}/{Model.PasteID}").ToString(); - string newUploadUrl = Model.SchemaName != null - ? new Uri(new Uri(Model.SectionUrl), Model.SchemaName).ToString() - : Model.SectionUrl; + string newUploadUrl = Model.SchemaName != null ? new Uri(new Uri(Model.SectionUrl), Model.SchemaName).ToString() : Model.SectionUrl; + string schemaDisplayName = null; + bool isValidSchema = Model.SchemaName != null && Model.SchemaFormats.TryGetValue(Model.SchemaName, out schemaDisplayName) && schemaDisplayName != "None"; + + // build title + ViewData["Title"] = "JSON validator"; + @if (Model.PasteID != null) + { + ViewData["ViewTitle"] = ViewData["Title"]; + ViewData["Title"] += + " (" + + string.Join(", ", new[] { isValidSchema ? schemaDisplayName : null, Model.PasteID }.Where(p => p != null)) + + ")"; + } } @section Head { @@ -130,7 +140,12 @@ else if (Model.PasteID != null) } } -

    Raw content

    +

    Content

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

    @ViewData["Title"]

    +

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

    @RenderBody()
    @error.Line @error.Path @error.Message
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    fieldsummary
    mods + +The mods for which to fetch metadata. Included fields: + + +field | summary +----- | ------- +`id` | The unique ID in the mod's `manifest.json`. This is used to crossreference with the wiki, and to index mods in the response. If it's unknown (e.g. you just have an update key), you can use a unique fake ID like `FAKE.Nexus.2400`. +`updateKeys` | _(optional)_ [Update keys](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest#Update_checks) which specify the mod pages to check, in addition to any mod pages linked to the `ID`. +`installedVersion` | _(optional)_ The installed version of the mod. If not specified, the API won't recommend an update. +`isBroken` | _(optional)_ Whether SMAPI failed to load the installed version of the mod, e.g. due to incompatibility. If true, the web API will be more permissive when recommending updates (e.g. allowing a stable → prerelease update). + +
    apiVersion + +_(optional)_ The installed version of SMAPI. If not specified, the API won't recommend an update. + +
    gameVersion + +_(optional)_ The installed version of Stardew Valley. This may be used to select updates. + +
    platform + +_(optional)_ The player's OS (`Android`, `Linux`, `Mac`, or `Windows`). This may be used to select updates. + +
    includeExtendedMetadata + +_(optional)_ Whether to include extra metadata that's not needed for SMAPI update checks, but which +may be useful to external tools. + +
    + +Example request: +```js +POST https://api.smapi.io/v3.0/mods { "mods": [ { - "id": "Pathoschild.LookupAnything", - "updateKeys": [ "nexus:541", "chucklefish:4250" ] + "id": "Pathoschild.ContentPatcher", + "updateKeys": [ "nexus:1915" ], + "installedVersion": "1.9.2", + "isBroken": false } ], + "apiVersion": "3.0.0", + "gameVersion": "1.4.0", + "platform": "Windows", "includeExtendedMetadata": true } ``` -The API will automatically aggregate versions and errors. Each mod will include... -* an `id` (matching what you passed in); -* up to three versions: `main` (e.g. 'latest version' field on Nexus), `optional` if newer (e.g. - optional files on Nexus), and `unofficial` if newer (from the wiki); -* `metadata` with mod info crossreferenced from the wiki and internal data (only if you specified - `includeExtendedMetadata: true`); -* and `errors` containing any error messages that occurred while fetching data. - -For example: +Response fields: + + + + + + + + + + + + + + + + + + + + + + + + + + +
    fieldsummary
    id + +The mod ID you specified in the request. + +
    suggestedUpdate + +The update version recommended by the web API, if any. This is based on some internal rules (e.g. +it won't recommend a prerelease update if the player has a working stable version) and context +(e.g. whether the player is in the game beta channel). Choosing an update version yourself isn't +recommended, but you can set `includeExtendedMetadata: true` and check the `metadata` field if you +really want to do that. + +
    errors + +Human-readable errors that occurred fetching the version info (e.g. if a mod page has an invalid +version). + +
    metadata + +Extra metadata that's not needed for SMAPI update checks but which may be useful to external tools, +if you set `includeExtendedMetadata: true` in the request. Included fields: + +field | summary +----- | ------- +`id` | The known `manifest.json` unique IDs for this mod defined on the wiki, if any. That includes historical versions of the mod. +`name` | The normalised name for this mod based on the crossreferenced sites. +`nexusID` | The mod ID on [Nexus Mods](https://www.nexusmods.com/stardewvalley/), if any. +`chucklefishID` | The mod ID in the [Chucklefish mod repo](https://community.playstarbound.com/resources/categories/stardew-valley.22/), if any. +`curseForgeID` | The mod project ID on [CurseForge](https://www.curseforge.com/stardewvalley), if any. +`curseForgeKey` | The mod key on [CurseForge](https://www.curseforge.com/stardewvalley), if any. This is used in the mod page URL. +`modDropID` | The mod ID on [ModDrop](https://www.moddrop.com/stardew-valley), if any. +`gitHubRepo` | The GitHub repository containing the mod code, if any. Specified in the `Owner/Repo` form. +`customSourceUrl` | The custom URL to the mod code, if any. This is used for mods which aren't stored in a GitHub repo. +`customUrl` | The custom URL to the mod page, if any. This is used for mods which aren't stored on one of the standard mod sites covered by the ID fields. +`main` | The primary mod version, if any. This depends on the mod site, but it's typically either the version of the mod itself or of its latest non-optional download. +`optional` | The latest optional download version, if any. +`unofficial` | The version of the unofficial update defined on the wiki for this mod, if any. +`unofficialForBeta` | Equivalent to `unofficial`, but for beta versions of SMAPI or Stardew Valley. +`hasBetaInfo` | Whether there's an ongoing Stardew Valley or SMAPI beta which may affect update checks. +`compatibilityStatus` | The compatibility status for the mod for the stable version of the game, as defined on the wiki, if any. See [possible values](https://github.com/Pathoschild/SMAPI/blob/develop/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs). +`compatibilitySummary` | The human-readable summary of the mod's compatibility in HTML format, if any. +`brokeIn` | The SMAPI or Stardew Valley version that broke this mod, if any. +`betaCompatibilityStatus`
    `betaCompatibilitySummary`
    `betaBrokeIn` | Equivalent to the preceding fields, but for beta versions of SMAPI or Stardew Valley. + + +
    + +Example response with `includeExtendedMetadata: false`: +```js +[ + { + "id": "Pathoschild.ContentPatcher", + "suggestedUpdate": { + "version": "1.10.0", + "url": "https://www.nexusmods.com/stardewvalley/mods/1915" + }, + "errors": [] + } +] ``` + +Example response with `includeExtendedMetadata: true`: +```js [ { - "id": "Pathoschild.LookupAnything", - "main": { - "version": "1.19", - "url": "https://www.nexusmods.com/stardewvalley/mods/541" + "id": "Pathoschild.ContentPatcher", + "suggestedUpdate": { + "version": "1.10.0", + "url": "https://www.nexusmods.com/stardewvalley/mods/1915" }, "metadata": { - "id": [ - "Pathoschild.LookupAnything", - "LookupAnything" - ], - "name": "Lookup Anything", - "nexusID": 541, + "id": [ "Pathoschild.ContentPatcher" ], + "name": "Content Patcher", + "nexusID": 1915, + "curseForgeID": 309243, + "curseForgeKey": "content-patcher", + "modDropID": 470174, "gitHubRepo": "Pathoschild/StardewMods", + "main": { + "version": "1.10", + "url": "https://www.nexusmods.com/stardewvalley/mods/1915" + }, + "hasBetaInfo": true, "compatibilityStatus": "Ok", "compatibilitySummary": "✓ use latest version." }, diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs index 8a9c0a25..f1bcfccc 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs @@ -1,3 +1,5 @@ +using System; + namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi { /// Metadata about a mod. @@ -9,23 +11,31 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// The mod's unique ID (if known). public string ID { get; set; } + /// The update version recommended by the web API based on its version update and mapping rules. + public ModEntryVersionModel SuggestedUpdate { get; set; } + + /// Optional extended data which isn't needed for update checks. + public ModExtendedMetadataModel Metadata { get; set; } + /// The main version. + [Obsolete] public ModEntryVersionModel Main { get; set; } /// The latest optional version, if newer than . + [Obsolete] public ModEntryVersionModel Optional { get; set; } /// The latest unofficial version, if newer than and . + [Obsolete] public ModEntryVersionModel Unofficial { get; set; } /// The latest unofficial version for the current Stardew Valley or SMAPI beta, if any (see ). + [Obsolete] public ModEntryVersionModel UnofficialForBeta { get; set; } - /// Optional extended data which isn't needed for update checks. - public ModExtendedMetadataModel Metadata { get; set; } - /// Whether a Stardew Valley or SMAPI beta which affects mod compatibility is in progress. If this is true, should be used for beta versions of SMAPI instead of . - public bool HasBetaInfo { get; set; } + [Obsolete] + public bool? HasBetaInfo { get; set; } /// The errors that occurred while fetching update data. public string[] Errors { get; set; } = new string[0]; diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs index 8074210c..4a697585 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs @@ -46,6 +46,17 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// The custom mod page URL (if applicable). public string CustomUrl { get; set; } + /// The main version. + public ModEntryVersionModel Main { get; set; } + + /// The latest optional version, if newer than . + public ModEntryVersionModel Optional { get; set; } + + /// The latest unofficial version, if newer than and . + public ModEntryVersionModel Unofficial { get; set; } + + /// The latest unofficial version for the current Stardew Valley or SMAPI beta, if any (see ). + public ModEntryVersionModel UnofficialForBeta { get; set; } /**** ** Stable compatibility @@ -60,7 +71,6 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// The game or SMAPI version which broke this mod, if applicable. public string BrokeIn { get; set; } - /**** ** Beta compatibility ****/ @@ -84,8 +94,18 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// Construct an instance. /// The mod metadata from the wiki (if available). /// The mod metadata from SMAPI's internal DB (if available). - public ModExtendedMetadataModel(WikiModEntry wiki, ModDataRecord db) + /// The main version. + /// The latest optional version, if newer than . + /// The latest unofficial version, if newer than and . + /// The latest unofficial version for the current Stardew Valley or SMAPI beta, if any. + public ModExtendedMetadataModel(WikiModEntry wiki, ModDataRecord db, ModEntryVersionModel main, ModEntryVersionModel optional, ModEntryVersionModel unofficial, ModEntryVersionModel unofficialForBeta) { + // versions + this.Main = main; + this.Optional = optional; + this.Unofficial = unofficial; + this.UnofficialForBeta = unofficialForBeta; + // wiki data if (wiki != null) { diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs deleted file mode 100644 index a2eaad16..00000000 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Linq; - -namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi -{ - /// Specifies mods whose update-check info to fetch. - public class ModSearchModel - { - /********* - ** Accessors - *********/ - /// The mods for which to find data. - public ModSearchEntryModel[] Mods { get; set; } - - /// Whether to include extended metadata for each mod. - public bool IncludeExtendedMetadata { get; set; } - - - /********* - ** Public methods - *********/ - /// Construct an empty instance. - public ModSearchModel() - { - // needed for JSON deserializing - } - - /// Construct an instance. - /// The mods to search. - /// Whether to include extended metadata for each mod. - public ModSearchModel(ModSearchEntryModel[] mods, bool includeExtendedMetadata) - { - this.Mods = mods.ToArray(); - this.IncludeExtendedMetadata = includeExtendedMetadata; - } - } -} diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs index 886cd5a1..bf81e102 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs @@ -12,6 +12,12 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// The namespaced mod update keys (if available). public string[] UpdateKeys { get; set; } + /// The mod version installed by the local player. This is used for version mapping in some cases. + public ISemanticVersion InstalledVersion { get; set; } + + /// Whether the installed version is broken or could not be loaded. + public bool IsBroken { get; set; } + /********* ** Public methods @@ -24,10 +30,13 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// Construct an instance. /// The unique mod ID. + /// The version installed by the local player. This is used for version mapping in some cases. /// The namespaced mod update keys (if available). - public ModSearchEntryModel(string id, string[] updateKeys) + /// Whether the installed version is broken or could not be loaded. + public ModSearchEntryModel(string id, ISemanticVersion installedVersion, string[] updateKeys, bool isBroken = false) { this.ID = id; + this.InstalledVersion = installedVersion; this.UpdateKeys = updateKeys ?? new string[0]; } } diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchModel.cs new file mode 100644 index 00000000..73698173 --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchModel.cs @@ -0,0 +1,52 @@ +using System.Linq; +using StardewModdingAPI.Toolkit.Utilities; + +namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi +{ + /// Specifies mods whose update-check info to fetch. + public class ModSearchModel + { + /********* + ** Accessors + *********/ + /// The mods for which to find data. + public ModSearchEntryModel[] Mods { get; set; } + + /// Whether to include extended metadata for each mod. + public bool IncludeExtendedMetadata { get; set; } + + /// The SMAPI version installed by the player. This is used for version mapping in some cases. + public ISemanticVersion ApiVersion { get; set; } + + /// The Stardew Valley version installed by the player. + public ISemanticVersion GameVersion { get; set; } + + /// The OS on which the player plays. + public Platform? Platform { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an empty instance. + public ModSearchModel() + { + // needed for JSON deserializing + } + + /// Construct an instance. + /// The mods to search. + /// The SMAPI version installed by the player. If this is null, the API won't provide a recommended update. + /// The Stardew Valley version installed by the player. + /// The OS on which the player plays. + /// Whether to include extended metadata for each mod. + public ModSearchModel(ModSearchEntryModel[] mods, ISemanticVersion apiVersion, ISemanticVersion gameVersion, Platform platform, bool includeExtendedMetadata) + { + this.Mods = mods.ToArray(); + this.ApiVersion = apiVersion; + this.GameVersion = gameVersion; + this.Platform = platform; + this.IncludeExtendedMetadata = includeExtendedMetadata; + } + } +} diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs index 80c8f62b..f0a7c82a 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Net; using Newtonsoft.Json; using StardewModdingAPI.Toolkit.Serialization; +using StardewModdingAPI.Toolkit.Utilities; namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi { @@ -37,12 +38,15 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// Get metadata about a set of mods from the web API. /// The mod keys for which to fetch the latest version. + /// The SMAPI version installed by the player. If this is null, the API won't provide a recommended update. + /// The Stardew Valley version installed by the player. + /// The OS on which the player plays. /// Whether to include extended metadata for each mod. - public IDictionary GetModInfo(ModSearchEntryModel[] mods, bool includeExtendedMetadata = false) + public IDictionary GetModInfo(ModSearchEntryModel[] mods, ISemanticVersion apiVersion, ISemanticVersion gameVersion, Platform platform, bool includeExtendedMetadata = false) { return this.Post( $"v{this.Version}/mods", - new ModSearchModel(mods, includeExtendedMetadata) + new ModSearchModel(mods, apiVersion, gameVersion, platform, includeExtendedMetadata) ).ToDictionary(p => p.ID); } diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs index 610e14f1..384f23fc 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs @@ -102,6 +102,8 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki string anchor = this.GetAttribute(node, "id"); string contentPackFor = this.GetAttribute(node, "data-content-pack-for"); string devNote = this.GetAttribute(node, "data-dev-note"); + IDictionary mapLocalVersions = this.GetAttributeAsVersionMapping(node, "data-map-local-versions"); + IDictionary mapRemoteVersions = this.GetAttributeAsVersionMapping(node, "data-map-remote-versions"); // parse stable compatibility WikiCompatibilityInfo compatibility = new WikiCompatibilityInfo @@ -159,6 +161,8 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki Warnings = warnings, MetadataLinks = metadataLinks.ToArray(), DevNote = devNote, + MapLocalVersions = mapLocalVersions, + MapRemoteVersions = mapRemoteVersions, Anchor = anchor }; } @@ -223,6 +227,28 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki return null; } + /// Get an attribute value and parse it as a version mapping. + /// The element whose attributes to read. + /// The attribute name. + private IDictionary GetAttributeAsVersionMapping(HtmlNode element, string name) + { + // get raw value + string raw = this.GetAttribute(element, name); + if (raw?.Contains("→") != true) + return null; + + // parse + // Specified on the wiki in the form "remote version → mapped version; another remote version → mapped version" + IDictionary map = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + foreach (string pair in raw.Split(';')) + { + string[] versions = pair.Split('→'); + if (versions.Length == 2 && !string.IsNullOrWhiteSpace(versions[0]) && !string.IsNullOrWhiteSpace(versions[1])) + map[versions[0].Trim()] = versions[1].Trim(); + } + return map; + } + /// Get the text of an element with the given class name. /// The metadata container. /// The field name. diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs index 51bb2336..931dcd43 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki { @@ -62,6 +63,12 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki /// Special notes intended for developers who maintain unofficial updates or submit pull requests. public string DevNote { get; set; } + /// Maps local versions to a semantic version for update checks. + public IDictionary MapLocalVersions { get; set; } + + /// Maps remote versions to a semantic version for update checks. + public IDictionary MapRemoteVersions { get; set; } + /// The link anchor for the mod entry in the wiki compatibility list. public string Anchor { get; set; } } diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDataModel.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDataModel.cs index dd0bd07b..8b40c301 100644 --- a/src/SMAPI.Toolkit/Framework/ModData/ModDataModel.cs +++ b/src/SMAPI.Toolkit/Framework/ModData/ModDataModel.cs @@ -25,12 +25,6 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData /// public string FormerIDs { get; set; } - /// Maps local versions to a semantic version for update checks. - public IDictionary MapLocalVersions { get; set; } = new Dictionary(); - - /// Maps remote versions to a semantic version for update checks. - public IDictionary MapRemoteVersions { get; set; } = new Dictionary(); - /// The mod warnings to suppress, even if they'd normally be shown. public ModWarning SuppressWarnings { get; set; } diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs index f01ada7c..c892d820 100644 --- a/src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs +++ b/src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs @@ -22,12 +22,6 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData /// The mod warnings to suppress, even if they'd normally be shown. public ModWarning SuppressWarnings { get; set; } - /// Maps local versions to a semantic version for update checks. - public IDictionary MapLocalVersions { get; } - - /// Maps remote versions to a semantic version for update checks. - public IDictionary MapRemoteVersions { get; } - /// The versioned field data. public ModDataField[] Fields { get; } @@ -44,8 +38,6 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData this.ID = model.ID; this.FormerIDs = model.GetFormerIDs().ToArray(); this.SuppressWarnings = model.SuppressWarnings; - this.MapLocalVersions = new Dictionary(model.MapLocalVersions, StringComparer.InvariantCultureIgnoreCase); - this.MapRemoteVersions = new Dictionary(model.MapRemoteVersions, StringComparer.InvariantCultureIgnoreCase); this.Fields = model.GetFields().ToArray(); } @@ -67,29 +59,6 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData return false; } - /// Get a semantic local version for update checks. - /// The remote version to normalize. - public ISemanticVersion GetLocalVersionForUpdateChecks(ISemanticVersion version) - { - return this.MapLocalVersions != null && this.MapLocalVersions.TryGetValue(version.ToString(), out string newVersion) - ? new SemanticVersion(newVersion) - : version; - } - - /// Get a semantic remote version for update checks. - /// The remote version to normalize. - public string GetRemoteVersionForUpdateChecks(string version) - { - // normalize version if possible - if (SemanticVersion.TryParse(version, out ISemanticVersion parsed)) - version = parsed.ToString(); - - // fetch remote version - return this.MapRemoteVersions != null && this.MapRemoteVersions.TryGetValue(version, out string newVersion) - ? newVersion - : version; - } - /// Get the possible mod IDs. public IEnumerable GetIDs() { diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs index 9e22990d..598da66a 100644 --- a/src/SMAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs +++ b/src/SMAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs @@ -26,29 +26,5 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData /// The upper version for which the applies (if any). public ISemanticVersion StatusUpperVersion { get; set; } - - - /********* - ** Public methods - *********/ - /// Get a semantic local version for update checks. - /// The remote version to normalize. - public ISemanticVersion GetLocalVersionForUpdateChecks(ISemanticVersion version) - { - return this.DataRecord.GetLocalVersionForUpdateChecks(version); - } - - /// Get a semantic remote version for update checks. - /// The remote version to normalize. - public ISemanticVersion GetRemoteVersionForUpdateChecks(ISemanticVersion version) - { - if (version == null) - return null; - - string rawVersion = this.DataRecord.GetRemoteVersionForUpdateChecks(version.ToString()); - return rawVersion != null - ? new SemanticVersion(rawVersion) - : version; - } } } diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index f65b164f..fe220eb5 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -80,7 +80,7 @@ namespace StardewModdingAPI.Web.Controllers new IModRepository[] { new ChucklefishRepository(chucklefish), - new CurseForgeRepository(curseForge), + new CurseForgeRepository(curseForge), new GitHubRepository(github), new ModDropRepository(modDrop), new NexusRepository(nexus) @@ -90,12 +90,15 @@ namespace StardewModdingAPI.Web.Controllers /// Fetch version metadata for the given mods. /// The mod search criteria. + /// The requested API version. [HttpPost] - public async Task> PostAsync([FromBody] ModSearchModel model) + public async Task> PostAsync([FromBody] ModSearchModel model, [FromRoute] string version) { if (model?.Mods == null) return new ModEntryModel[0]; + bool legacyMode = SemanticVersion.TryParse(version, out ISemanticVersion parsedVersion) && parsedVersion.IsOlderThan("3.0.0-beta.20191109"); + // fetch wiki data WikiModEntry[] wikiData = this.WikiCache.GetWikiMods().Select(p => p.GetModel()).ToArray(); IDictionary mods = new Dictionary(StringComparer.CurrentCultureIgnoreCase); @@ -104,7 +107,25 @@ namespace StardewModdingAPI.Web.Controllers if (string.IsNullOrWhiteSpace(mod.ID)) continue; - ModEntryModel result = await this.GetModData(mod, wikiData, model.IncludeExtendedMetadata); + ModEntryModel result = await this.GetModData(mod, wikiData, model.IncludeExtendedMetadata || legacyMode, model.ApiVersion); + if (legacyMode) + { + result.Main = result.Metadata.Main; + result.Optional = result.Metadata.Optional; + result.Unofficial = result.Metadata.Unofficial; + result.UnofficialForBeta = result.Metadata.UnofficialForBeta; + result.HasBetaInfo = result.Metadata.BetaCompatibilityStatus != null; + result.SuggestedUpdate = null; + if (!model.IncludeExtendedMetadata) + result.Metadata = null; + } + else if (!model.IncludeExtendedMetadata && (model.ApiVersion == null || mod.InstalledVersion == null)) + { + var errors = new List(result.Errors); + errors.Add($"This API can't suggest an update because {nameof(model.ApiVersion)} or {nameof(mod.InstalledVersion)} are null, and you didn't specify {nameof(model.IncludeExtendedMetadata)} to get other info. See the SMAPI technical docs for usage."); + result.Errors = errors.ToArray(); + } + mods[mod.ID] = result; } @@ -120,8 +141,9 @@ namespace StardewModdingAPI.Web.Controllers /// The mod data to match. /// The wiki data. /// Whether to include extended metadata for each mod. + /// The SMAPI version installed by the player. /// Returns the mod data if found, else null. - private async Task GetModData(ModSearchEntryModel search, WikiModEntry[] wikiData, bool includeExtendedMetadata) + private async Task GetModData(ModSearchEntryModel search, WikiModEntry[] wikiData, bool includeExtendedMetadata, ISemanticVersion apiVersion) { // cross-reference data ModDataRecord record = this.ModDatabase.Get(search.ID); @@ -131,6 +153,10 @@ namespace StardewModdingAPI.Web.Controllers // get latest versions ModEntryModel result = new ModEntryModel { ID = search.ID }; IList errors = new List(); + ModEntryVersionModel main = null; + ModEntryVersionModel optional = null; + ModEntryVersionModel unofficial = null; + ModEntryVersionModel unofficialForBeta = null; foreach (UpdateKey updateKey in updateKeys) { // validate update key @@ -151,76 +177,118 @@ namespace StardewModdingAPI.Web.Controllers // handle main version if (data.Version != null) { - if (!SemanticVersion.TryParse(data.Version, out ISemanticVersion version)) + ISemanticVersion version = this.GetMappedVersion(data.Version, wikiEntry?.MapRemoteVersions); + if (version == null) { errors.Add($"The update key '{updateKey}' matches a mod with invalid semantic version '{data.Version}'."); continue; } - if (this.IsNewer(version, result.Main?.Version)) - result.Main = new ModEntryVersionModel(version, data.Url); + if (this.IsNewer(version, main?.Version)) + main = new ModEntryVersionModel(version, data.Url); } // handle optional version if (data.PreviewVersion != null) { - if (!SemanticVersion.TryParse(data.PreviewVersion, out ISemanticVersion version)) + ISemanticVersion version = this.GetMappedVersion(data.PreviewVersion, wikiEntry?.MapRemoteVersions); + if (version == null) { errors.Add($"The update key '{updateKey}' matches a mod with invalid optional semantic version '{data.PreviewVersion}'."); continue; } - if (this.IsNewer(version, result.Optional?.Version)) - result.Optional = new ModEntryVersionModel(version, data.Url); + if (this.IsNewer(version, optional?.Version)) + optional = new ModEntryVersionModel(version, data.Url); } } // get unofficial version - if (wikiEntry?.Compatibility.UnofficialVersion != null && this.IsNewer(wikiEntry.Compatibility.UnofficialVersion, result.Main?.Version) && this.IsNewer(wikiEntry.Compatibility.UnofficialVersion, result.Optional?.Version)) - result.Unofficial = new ModEntryVersionModel(wikiEntry.Compatibility.UnofficialVersion, $"{this.CompatibilityPageUrl}/#{wikiEntry.Anchor}"); + if (wikiEntry?.Compatibility.UnofficialVersion != null && this.IsNewer(wikiEntry.Compatibility.UnofficialVersion, main?.Version) && this.IsNewer(wikiEntry.Compatibility.UnofficialVersion, optional?.Version)) + unofficial = new ModEntryVersionModel(wikiEntry.Compatibility.UnofficialVersion, $"{this.CompatibilityPageUrl}/#{wikiEntry.Anchor}"); // get unofficial version for beta if (wikiEntry?.HasBetaInfo == true) { - result.HasBetaInfo = true; if (wikiEntry.BetaCompatibility.Status == WikiCompatibilityStatus.Unofficial) { if (wikiEntry.BetaCompatibility.UnofficialVersion != null) { - result.UnofficialForBeta = (wikiEntry.BetaCompatibility.UnofficialVersion != null && this.IsNewer(wikiEntry.BetaCompatibility.UnofficialVersion, result.Main?.Version) && this.IsNewer(wikiEntry.BetaCompatibility.UnofficialVersion, result.Optional?.Version)) + unofficialForBeta = (wikiEntry.BetaCompatibility.UnofficialVersion != null && this.IsNewer(wikiEntry.BetaCompatibility.UnofficialVersion, main?.Version) && this.IsNewer(wikiEntry.BetaCompatibility.UnofficialVersion, optional?.Version)) ? new ModEntryVersionModel(wikiEntry.BetaCompatibility.UnofficialVersion, $"{this.CompatibilityPageUrl}/#{wikiEntry.Anchor}") : null; } else - result.UnofficialForBeta = result.Unofficial; + unofficialForBeta = unofficial; } } // fallback to preview if latest is invalid - if (result.Main == null && result.Optional != null) + if (main == null && optional != null) { - result.Main = result.Optional; - result.Optional = null; + main = optional; + optional = null; } // special cases if (result.ID == "Pathoschild.SMAPI") { - if (result.Main != null) - result.Main.Url = "https://smapi.io/"; - if (result.Optional != null) - result.Optional.Url = "https://smapi.io/"; + if (main != null) + main.Url = "https://smapi.io/"; + if (optional != null) + optional.Url = "https://smapi.io/"; + } + + // get recommended update (if any) + ISemanticVersion installedVersion = this.GetMappedVersion(search.InstalledVersion?.ToString(), wikiEntry?.MapLocalVersions); + if (apiVersion != null && installedVersion != null) + { + // get newer versions + List updates = new List(); + if (this.IsRecommendedUpdate(installedVersion, main?.Version, useBetaChannel: true)) + updates.Add(main); + if (this.IsRecommendedUpdate(installedVersion, optional?.Version, useBetaChannel: installedVersion.IsPrerelease())) + updates.Add(optional); + if (this.IsRecommendedUpdate(installedVersion, unofficial?.Version, useBetaChannel: search.IsBroken)) + updates.Add(unofficial); + if (this.IsRecommendedUpdate(installedVersion, unofficialForBeta?.Version, useBetaChannel: apiVersion.IsPrerelease())) + updates.Add(unofficialForBeta); + + // get newest version + ModEntryVersionModel newest = null; + foreach (ModEntryVersionModel update in updates) + { + if (newest == null || update.Version.IsNewerThan(newest.Version)) + newest = update; + } + + // set field + result.SuggestedUpdate = newest != null + ? new ModEntryVersionModel(newest.Version, newest.Url) + : null; } // add extended metadata - if (includeExtendedMetadata && (wikiEntry != null || record != null)) - result.Metadata = new ModExtendedMetadataModel(wikiEntry, record); + if (includeExtendedMetadata) + result.Metadata = new ModExtendedMetadataModel(wikiEntry, record, main: main, optional: optional, unofficial: unofficial, unofficialForBeta: unofficialForBeta); // add result result.Errors = errors.ToArray(); return result; } + /// Get whether a given version should be offered to the user as an update. + /// The current semantic version. + /// The target semantic version. + /// Whether the user enabled the beta channel and should be offered prerelease updates. + private bool IsRecommendedUpdate(ISemanticVersion currentVersion, ISemanticVersion newVersion, bool useBetaChannel) + { + return + newVersion != null + && newVersion.IsNewerThan(currentVersion) + && (useBetaChannel || !newVersion.IsPrerelease()); + } + /// Get whether a version is newer than an version. /// The current version. /// The other version. @@ -260,7 +328,7 @@ namespace StardewModdingAPI.Web.Controllers /// The specified update keys. /// The mod's entry in SMAPI's internal database. /// The mod's entry in the wiki list. - public IEnumerable GetUpdateKeys(string[] specifiedKeys, ModDataRecord record, WikiModEntry entry) + private IEnumerable GetUpdateKeys(string[] specifiedKeys, ModDataRecord record, WikiModEntry entry) { IEnumerable GetRaw() { @@ -301,5 +369,49 @@ namespace StardewModdingAPI.Web.Controllers yield return key; } } + + /// Get a semantic local version for update checks. + /// The version to parse. + /// A map of version replacements. + private ISemanticVersion GetMappedVersion(string version, IDictionary map) + { + // try mapped version + string rawNewVersion = this.GetRawMappedVersion(version, map); + if (SemanticVersion.TryParse(rawNewVersion, out ISemanticVersion parsedNew)) + return parsedNew; + + // return original version + return SemanticVersion.TryParse(version, out ISemanticVersion parsedOld) + ? parsedOld + : null; + } + + /// Get a semantic local version for update checks. + /// The version to map. + /// A map of version replacements. + private string GetRawMappedVersion(string version, IDictionary map) + { + if (version == null || map == null || !map.Any()) + return version; + + // match exact raw version + if (map.ContainsKey(version)) + return map[version]; + + // match parsed version + if (SemanticVersion.TryParse(version, out ISemanticVersion parsed)) + { + if (map.ContainsKey(parsed.ToString())) + return map[parsed.ToString()]; + + foreach (var pair in map) + { + if (SemanticVersion.TryParse(pair.Key, out ISemanticVersion target) && parsed.Equals(target) && SemanticVersion.TryParse(pair.Value, out ISemanticVersion newVersion)) + return newVersion.ToString(); + } + } + + return version; + } } } diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs b/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs index fddf99ee..8569984a 100644 --- a/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs +++ b/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs @@ -1,6 +1,9 @@ using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Bson.Serialization.Options; using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; @@ -109,6 +112,17 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki /// The URL to the latest unofficial update, if applicable. public string BetaUnofficialUrl { get; set; } + /**** + ** Version maps + ****/ + /// Maps local versions to a semantic version for update checks. + [BsonDictionaryOptions(Representation = DictionaryRepresentation.ArrayOfArrays)] + public IDictionary MapLocalVersions { get; set; } + + /// Maps remote versions to a semantic version for update checks. + [BsonDictionaryOptions(Representation = DictionaryRepresentation.ArrayOfArrays)] + public IDictionary MapRemoteVersions { get; set; } + /********* ** Accessors @@ -154,6 +168,10 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki this.BetaBrokeIn = mod.BetaCompatibility?.BrokeIn; this.BetaUnofficialVersion = mod.BetaCompatibility?.UnofficialVersion?.ToString(); this.BetaUnofficialUrl = mod.BetaCompatibility?.UnofficialUrl; + + // version maps + this.MapLocalVersions = mod.MapLocalVersions; + this.MapRemoteVersions = mod.MapRemoteVersions; } /// Reconstruct the original model. @@ -186,7 +204,11 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki BrokeIn = this.MainBrokeIn, UnofficialVersion = this.MainUnofficialVersion != null ? new SemanticVersion(this.MainUnofficialVersion) : null, UnofficialUrl = this.MainUnofficialUrl - } + }, + + // version maps + MapLocalVersions = this.MapLocalVersions, + MapRemoteVersions = this.MapRemoteVersions }; // beta compatibility diff --git a/src/SMAPI.Web/Views/Index/Privacy.cshtml b/src/SMAPI.Web/Views/Index/Privacy.cshtml index 356580b4..914384a8 100644 --- a/src/SMAPI.Web/Views/Index/Privacy.cshtml +++ b/src/SMAPI.Web/Views/Index/Privacy.cshtml @@ -24,7 +24,7 @@

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

    Update checks

    -

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

    +

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

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

      diff --git a/src/SMAPI.Web/wwwroot/SMAPI.metadata.json b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json index e4c0a6f6..553b6934 100644 --- a/src/SMAPI.Web/wwwroot/SMAPI.metadata.json +++ b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json @@ -14,11 +14,6 @@ * other fields if no ID was specified. This doesn't include the latest ID, if any. Multiple * variants can be separated with '|'. * - * - MapLocalVersions and MapRemoteVersions correct local manifest versions and remote versions - * during update checks. For example, if the API returns version '1.1-1078' where '1078' is - * intended to be a build number, MapRemoteVersions can map it to '1.1' when comparing to the - * mod's current version. This is only meant to support legacy mods with injected update keys. - * * Versioned metadata * ================== * Each record can also specify extra metadata using the field keys below. @@ -122,91 +117,6 @@ "Default | UpdateKey": "Nexus:1820" }, - - /********* - ** Map versions - *********/ - "Adjust Artisan Prices": { - "ID": "ThatNorthernMonkey.AdjustArtisanPrices", - "FormerIDs": "1e36d4ca-c7ef-4dfb-9927-d27a6c3c8bdc", // changed in 0.0.2-pathoschild-update - "MapRemoteVersions": { "0.01": "0.0.1" } - }, - - "Almighty Farming Tool": { - "ID": "439", - "MapRemoteVersions": { - "1.21": "1.2.1", - "1.22-unofficial.3.mizzion": "1.2.2-unofficial.3.mizzion" - } - }, - - "Basic Sprinkler Improved": { - "ID": "lrsk_sdvm_bsi.0117171308", - "MapRemoteVersions": { "1.0.2": "1.0.1-release" } // manifest not updated - }, - - "Better Shipping Box": { - "ID": "Kithio:BetterShippingBox", - "MapLocalVersions": { "1.0.1": "1.0.2" } - }, - - "Chefs Closet": { - "ID": "Duder.ChefsCloset", - "MapLocalVersions": { "1.3-1": "1.3" } - }, - - "Configurable Machines": { - "ID": "21da6619-dc03-4660-9794-8e5b498f5b97", - "MapLocalVersions": { "1.2-beta": "1.2" } - }, - - "Crafting Counter": { - "ID": "lolpcgaming.CraftingCounter", - "MapRemoteVersions": { "1.1": "1.0" } // not updated in manifest - }, - - "Custom Linens": { - "ID": "Mevima.CustomLinens", - "MapRemoteVersions": { "1.1": "1.0" } // manifest not updated - }, - - "Dynamic Horses": { - "ID": "Bpendragon-DynamicHorses", - "MapRemoteVersions": { "1.2": "1.1-release" } // manifest not updated - }, - - "Dynamic Machines": { - "ID": "DynamicMachines", - "MapLocalVersions": { "1.1": "1.1.1" } - }, - - "Multiple Sprites and Portraits On Rotation (File Loading)": { - "ID": "FileLoading", - "MapLocalVersions": { "1.1": "1.12" } - }, - - "Relationship Status": { - "ID": "relationshipstatus", - "MapRemoteVersions": { "1.0.5": "1.0.4" } // not updated in manifest - }, - - "ReRegeneration": { - "ID": "lrsk_sdvm_rerg.0925160827", - "MapLocalVersions": { "1.1.2-release": "1.1.2" } - }, - - "Showcase Mod": { - "ID": "Igorious.Showcase", - "MapLocalVersions": { "0.9-500": "0.9" } - }, - - "Siv's Marriage Mod": { - "ID": "6266959802", // official version - "FormerIDs": "Siv.MarriageMod | medoli900.Siv's Marriage Mod", // 1.2.3-unofficial versions - "MapLocalVersions": { "0.0": "1.4" } - }, - - /********* ** Obsolete *********/ @@ -477,12 +387,6 @@ "~1.0.0 | Status": "AssumeBroken" // broke in Stardew Valley 1.3.29 (runtime errors) }, - "Skill Prestige: Cooking Adapter": { - "ID": "Alphablackwolf.CookingSkillPrestigeAdapter", - "FormerIDs": "20d6b8a3-b6e7-460b-a6e4-07c2b0cb6c63", // changed circa 1.1 - "MapRemoteVersions": { "1.2.3": "1.1" } // manifest not updated - }, - "Skull Cave Saver": { "ID": "cantorsdust.SkullCaveSaver", "FormerIDs": "8ac06349-26f7-4394-806c-95d48fd35774 | community.SkullCaveSaver", // changed in 1.1 and 1.2.2 @@ -501,7 +405,6 @@ "Stephan's Lots of Crops": { "ID": "stephansstardewcrops", - "MapRemoteVersions": { "1.41": "1.1" }, // manifest not updated "~1.1 | Status": "AssumeBroken" // broke in SDV 1.3 (overwrites vanilla items) }, diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 50bd562a..77b17c8a 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -593,27 +593,19 @@ namespace StardewModdingAPI.Framework ISemanticVersion updateFound = null; try { - ModEntryModel response = client.GetModInfo(new[] { new ModSearchEntryModel("Pathoschild.SMAPI", new[] { $"GitHub:{this.Settings.GitHubProjectName}" }) }).Single().Value; - ISemanticVersion latestStable = response.Main?.Version; - ISemanticVersion latestBeta = response.Optional?.Version; + // fetch update check + ModEntryModel response = client.GetModInfo(new[] { new ModSearchEntryModel("Pathoschild.SMAPI", Constants.ApiVersion, new[] { $"GitHub:{this.Settings.GitHubProjectName}" }) }, apiVersion: Constants.ApiVersion, gameVersion: Constants.GameVersion, platform: Constants.Platform).Single().Value; + if (response.SuggestedUpdate != null) + this.Monitor.Log($"You can update SMAPI to {response.SuggestedUpdate.Version}: {Constants.HomePageUrl}", LogLevel.Alert); + else + this.Monitor.Log(" SMAPI okay.", LogLevel.Trace); - if (latestStable == null && response.Errors.Any()) + // show errors + if (response.Errors.Any()) { this.Monitor.Log("Couldn't check for a new version of SMAPI. This won't affect your game, but you may not be notified of new versions if this keeps happening.", LogLevel.Warn); this.Monitor.Log($"Error: {string.Join("\n", response.Errors)}", LogLevel.Trace); } - else if (this.IsValidUpdate(Constants.ApiVersion, latestBeta, this.Settings.UseBetaChannel)) - { - updateFound = latestBeta; - this.Monitor.Log($"You can update SMAPI to {latestBeta}: {Constants.HomePageUrl}", LogLevel.Alert); - } - else if (this.IsValidUpdate(Constants.ApiVersion, latestStable, this.Settings.UseBetaChannel)) - { - updateFound = latestStable; - this.Monitor.Log($"You can update SMAPI to {latestStable}: {Constants.HomePageUrl}", LogLevel.Alert); - } - else - this.Monitor.Log(" SMAPI okay.", LogLevel.Trace); } catch (Exception ex) { @@ -646,12 +638,12 @@ namespace StardewModdingAPI.Framework .GetUpdateKeys(validOnly: true) .Select(p => p.ToString()) .ToArray(); - searchMods.Add(new ModSearchEntryModel(mod.Manifest.UniqueID, updateKeys.ToArray())); + searchMods.Add(new ModSearchEntryModel(mod.Manifest.UniqueID, mod.Manifest.Version, updateKeys.ToArray(), isBroken: mod.Status == ModMetadataStatus.Failed)); } // fetch results this.Monitor.Log($" Checking for updates to {searchMods.Count} mods...", LogLevel.Trace); - IDictionary results = client.GetModInfo(searchMods.ToArray()); + IDictionary results = client.GetModInfo(searchMods.ToArray(), apiVersion: Constants.ApiVersion, gameVersion: Constants.GameVersion, platform: Constants.Platform); // extract update alerts & errors var updates = new List>(); @@ -672,20 +664,9 @@ namespace StardewModdingAPI.Framework ); } - // parse versions - bool useBetaInfo = result.HasBetaInfo && Constants.ApiVersion.IsPrerelease(); - ISemanticVersion localVersion = mod.DataRecord?.GetLocalVersionForUpdateChecks(mod.Manifest.Version) ?? mod.Manifest.Version; - ISemanticVersion latestVersion = mod.DataRecord?.GetRemoteVersionForUpdateChecks(result.Main?.Version) ?? result.Main?.Version; - ISemanticVersion optionalVersion = mod.DataRecord?.GetRemoteVersionForUpdateChecks(result.Optional?.Version) ?? result.Optional?.Version; - ISemanticVersion unofficialVersion = useBetaInfo ? result.UnofficialForBeta?.Version : result.Unofficial?.Version; - - // show update alerts - if (this.IsValidUpdate(localVersion, latestVersion, useBetaChannel: true)) - updates.Add(Tuple.Create(mod, latestVersion, result.Main?.Url)); - else if (this.IsValidUpdate(localVersion, optionalVersion, useBetaChannel: localVersion.IsPrerelease())) - updates.Add(Tuple.Create(mod, optionalVersion, result.Optional?.Url)); - else if (this.IsValidUpdate(localVersion, unofficialVersion, useBetaChannel: mod.Status == ModMetadataStatus.Failed)) - updates.Add(Tuple.Create(mod, unofficialVersion, useBetaInfo ? result.UnofficialForBeta?.Url : result.Unofficial?.Url)); + // handle update + if (result.SuggestedUpdate != null) + updates.Add(Tuple.Create(mod, result.SuggestedUpdate.Version, result.SuggestedUpdate.Url)); } // show update errors @@ -720,18 +701,6 @@ namespace StardewModdingAPI.Framework }).Start(); } - /// Get whether a given version should be offered to the user as an update. - /// The current semantic version. - /// The target semantic version. - /// Whether the user enabled the beta channel and should be offered prerelease updates. - private bool IsValidUpdate(ISemanticVersion currentVersion, ISemanticVersion newVersion, bool useBetaChannel) - { - return - newVersion != null - && newVersion.IsNewerThan(currentVersion) - && (useBetaChannel || !newVersion.IsPrerelease()); - } - /// Create a directory path if it doesn't exist. /// The directory path. private void VerifyPath(string path) -- cgit From 7a2fc8471fe31f560ddcf5e5005b92b0cd0a7c57 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 14 Aug 2019 19:52:04 -0400 Subject: update for draw changes in SDV 1.4 (#638) --- src/SMAPI/Framework/SGame.cs | 179 ++++++++++++++++++------------------------- 1 file changed, 75 insertions(+), 104 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 13858fc5..cdd7437a 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -815,12 +815,20 @@ namespace StardewModdingAPI.Framework { var events = this.Events; + // from Game1.Draw + RenderTarget2D target_screen = (RenderTarget2D)null; + if ((double)Game1.options.zoomLevel != 1.0) + target_screen = this.screen; + + // from Game1._draw if (Game1._newDayTask != null) + { this.GraphicsDevice.Clear(Game1.bgColor); + } else { - if ((double)Game1.options.zoomLevel != 1.0) - this.GraphicsDevice.SetRenderTarget(this.screen); + if (target_screen != null) + this.GraphicsDevice.SetRenderTarget(target_screen); if (this.IsSaving) { this.GraphicsDevice.Clear(Game1.bgColor); @@ -874,12 +882,12 @@ namespace StardewModdingAPI.Framework events.Rendered.RaiseEmpty(); Game1.spriteBatch.End(); this.drawOverlays(Game1.spriteBatch); - if ((double)Game1.options.zoomLevel != 1.0) + if (target_screen != null) { this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); this.GraphicsDevice.Clear(Game1.bgColor); Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); - Game1.spriteBatch.Draw((Texture2D)this.screen, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screen.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); + Game1.spriteBatch.Draw((Texture2D)target_screen, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(target_screen.Bounds), Microsoft.Xna.Framework.Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); Game1.spriteBatch.End(); } if (Game1.overlayMenu == null) @@ -892,9 +900,9 @@ namespace StardewModdingAPI.Framework { Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); events.Rendering.RaiseEmpty(); - Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3685"), new Vector2(16f, 16f), Color.HotPink); - Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3686"), new Vector2(16f, 32f), new Color(0, (int)byte.MaxValue, 0)); - Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.parseText(Game1.errorMessage, Game1.dialogueFont, Game1.graphics.GraphicsDevice.Viewport.Width), new Vector2(16f, 48f), Color.White); + Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3685"), new Vector2(16f, 16f), Microsoft.Xna.Framework.Color.HotPink); + Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3686"), new Vector2(16f, 32f), new Microsoft.Xna.Framework.Color(0, (int)byte.MaxValue, 0)); + Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.parseText(Game1.errorMessage, Game1.dialogueFont, Game1.graphics.GraphicsDevice.Viewport.Width), new Vector2(16f, 48f), Microsoft.Xna.Framework.Color.White); events.Rendered.RaiseEmpty(); Game1.spriteBatch.End(); } @@ -904,16 +912,16 @@ namespace StardewModdingAPI.Framework if (Game1.globalFade && !Game1.menuUp && (!Game1.nameSelectUp || Game1.messagePause)) { Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * (Game1.gameMode == (byte)0 ? 1f - Game1.fadeToBlackAlpha : Game1.fadeToBlackAlpha)); + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Microsoft.Xna.Framework.Color.Black * (Game1.gameMode == (byte)0 ? 1f - Game1.fadeToBlackAlpha : Game1.fadeToBlackAlpha)); Game1.spriteBatch.End(); } this.drawOverlays(Game1.spriteBatch); - if ((double)Game1.options.zoomLevel == 1.0) + if (target_screen == null) return; this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); this.GraphicsDevice.Clear(Game1.bgColor); Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); - Game1.spriteBatch.Draw((Texture2D)this.screen, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screen.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); + Game1.spriteBatch.Draw((Texture2D)target_screen, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(target_screen.Bounds), Microsoft.Xna.Framework.Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); Game1.spriteBatch.End(); } else if (Game1.showingEndOfNightStuff) @@ -937,12 +945,12 @@ namespace StardewModdingAPI.Framework events.Rendered.RaiseEmpty(); Game1.spriteBatch.End(); this.drawOverlays(Game1.spriteBatch); - if ((double)Game1.options.zoomLevel == 1.0) + if (target_screen == null) return; this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); this.GraphicsDevice.Clear(Game1.bgColor); Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); - Game1.spriteBatch.Draw((Texture2D)this.screen, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screen.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); + Game1.spriteBatch.Draw((Texture2D)target_screen, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(target_screen.Bounds), Microsoft.Xna.Framework.Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); Game1.spriteBatch.End(); } else if (Game1.gameMode == (byte)6 || Game1.gameMode == (byte)3 && Game1.currentLocation == null) @@ -963,12 +971,12 @@ namespace StardewModdingAPI.Framework events.Rendered.RaiseEmpty(); Game1.spriteBatch.End(); this.drawOverlays(Game1.spriteBatch); - if ((double)Game1.options.zoomLevel != 1.0) + if (target_screen != null) { this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); this.GraphicsDevice.Clear(Game1.bgColor); Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); - Game1.spriteBatch.Draw((Texture2D)this.screen, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screen.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); + Game1.spriteBatch.Draw((Texture2D)target_screen, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(target_screen.Bounds), Microsoft.Xna.Framework.Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); Game1.spriteBatch.End(); } if (Game1.overlayMenu != null) @@ -995,12 +1003,12 @@ namespace StardewModdingAPI.Framework if (Game1.drawLighting) { this.GraphicsDevice.SetRenderTarget(Game1.lightmap); - this.GraphicsDevice.Clear(Color.White * 0.0f); + this.GraphicsDevice.Clear(Microsoft.Xna.Framework.Color.White * 0.0f); Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); if (++batchOpens == 1) events.Rendering.RaiseEmpty(); - Color color1 = !Game1.currentLocation.Name.StartsWith("UndergroundMine") || !(Game1.currentLocation is MineShaft) ? (Game1.ambientLight.Equals(Color.White) || Game1.isRaining && (bool)((NetFieldBase)Game1.currentLocation.isOutdoors) ? Game1.outdoorLight : Game1.ambientLight) : (Game1.currentLocation as MineShaft).getLightingColor(gameTime); - Game1.spriteBatch.Draw(Game1.staminaRect, Game1.lightmap.Bounds, color1); + Microsoft.Xna.Framework.Color color = !Game1.currentLocation.Name.StartsWith("UndergroundMine") || !(Game1.currentLocation is MineShaft) ? (Game1.ambientLight.Equals(Microsoft.Xna.Framework.Color.White) || Game1.isRaining && (bool)((NetFieldBase)Game1.currentLocation.isOutdoors) ? Game1.outdoorLight : Game1.ambientLight) : (Game1.currentLocation as MineShaft).getLightingColor(gameTime); + Game1.spriteBatch.Draw(Game1.staminaRect, Game1.lightmap.Bounds, color); for (int index = 0; index < Game1.currentLightSources.Count; ++index) { LightSource lightSource = Game1.currentLightSources.ElementAt(index); @@ -1011,23 +1019,10 @@ namespace StardewModdingAPI.Framework continue; } if (Utility.isOnScreen((Vector2)((NetFieldBase)Game1.currentLightSources.ElementAt(index).position), (int)((double)(float)((NetFieldBase)Game1.currentLightSources.ElementAt(index).radius) * 64.0 * 4.0))) - { - SpriteBatch spriteBatch = Game1.spriteBatch; - Texture2D lightTexture = Game1.currentLightSources.ElementAt(index).lightTexture; - Vector2 position = Game1.GlobalToLocal(Game1.viewport, (Vector2)((NetFieldBase)Game1.currentLightSources.ElementAt(index).position)) / (float)(Game1.options.lightingQuality / 2); - Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.currentLightSources.ElementAt(index).lightTexture.Bounds); - Color color2 = (Color)((NetFieldBase)Game1.currentLightSources.ElementAt(index).color); - Microsoft.Xna.Framework.Rectangle bounds = Game1.currentLightSources.ElementAt(index).lightTexture.Bounds; - double x = (double)bounds.Center.X; - bounds = Game1.currentLightSources.ElementAt(index).lightTexture.Bounds; - double y = (double)bounds.Center.Y; - Vector2 origin = new Vector2((float)x, (float)y); - double num = (double)(float)((NetFieldBase)Game1.currentLightSources.ElementAt(index).radius) / (double)(Game1.options.lightingQuality / 2); - spriteBatch.Draw(lightTexture, position, sourceRectangle, color2, 0.0f, origin, (float)num, SpriteEffects.None, 0.9f); - } + Game1.spriteBatch.Draw(Game1.currentLightSources.ElementAt(index).lightTexture, Game1.GlobalToLocal(Game1.viewport, (Vector2)((NetFieldBase)Game1.currentLightSources.ElementAt(index).position)) / (float)(Game1.options.lightingQuality / 2), new Microsoft.Xna.Framework.Rectangle?(Game1.currentLightSources.ElementAt(index).lightTexture.Bounds), (Microsoft.Xna.Framework.Color)((NetFieldBase)Game1.currentLightSources.ElementAt(index).color), 0.0f, new Vector2((float)Game1.currentLightSources.ElementAt(index).lightTexture.Bounds.Center.X, (float)Game1.currentLightSources.ElementAt(index).lightTexture.Bounds.Center.Y), (float)((NetFieldBase)Game1.currentLightSources.ElementAt(index).radius) / (float)(Game1.options.lightingQuality / 2), SpriteEffects.None, 0.9f); } Game1.spriteBatch.End(); - this.GraphicsDevice.SetRenderTarget((double)Game1.options.zoomLevel == 1.0 ? (RenderTarget2D)null : this.screen); + this.GraphicsDevice.SetRenderTarget(target_screen); } if (Game1.bloomDay && Game1.bloom != null) Game1.bloom.BeginDraw(); @@ -1065,7 +1060,7 @@ namespace StardewModdingAPI.Framework foreach (NPC character in Game1.currentLocation.characters) { if (!(bool)((NetFieldBase)character.swimming) && !character.HideShadow && (!character.IsInvisible && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(character.getTileLocation()))) - Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, character.Position + new Vector2((float)(character.Sprite.SpriteWidth * 4) / 2f, (float)(character.GetBoundingBox().Height + (character.IsMonster ? 0 : 12)))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)character.yJumpOffset / 40.0) * (float)((NetFieldBase)character.scale), SpriteEffects.None, Math.Max(0.0f, (float)character.getStandingY() / 10000f) - 1E-06f); + Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, character.Position + new Vector2((float)(character.Sprite.SpriteWidth * 4) / 2f, (float)(character.GetBoundingBox().Height + (character.IsMonster ? 0 : 12)))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Microsoft.Xna.Framework.Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)character.yJumpOffset / 40.0) * (float)((NetFieldBase)character.scale), SpriteEffects.None, Math.Max(0.0f, (float)character.getStandingY() / 10000f) - 1E-06f); } } else @@ -1073,26 +1068,13 @@ namespace StardewModdingAPI.Framework foreach (NPC actor in Game1.CurrentEvent.actors) { if (!(bool)((NetFieldBase)actor.swimming) && !actor.HideShadow && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(actor.getTileLocation())) - Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, actor.Position + new Vector2((float)(actor.Sprite.SpriteWidth * 4) / 2f, (float)(actor.GetBoundingBox().Height + (actor.IsMonster ? 0 : (actor.Sprite.SpriteHeight <= 16 ? -4 : 12))))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)actor.yJumpOffset / 40.0) * (float)((NetFieldBase)actor.scale), SpriteEffects.None, Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 1E-06f); + Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, actor.Position + new Vector2((float)(actor.Sprite.SpriteWidth * 4) / 2f, (float)(actor.GetBoundingBox().Height + (actor.IsMonster ? 0 : (actor.Sprite.SpriteHeight <= 16 ? -4 : 12))))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Microsoft.Xna.Framework.Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)actor.yJumpOffset / 40.0) * (float)((NetFieldBase)actor.scale), SpriteEffects.None, Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 1E-06f); } } foreach (Farmer farmerShadow in this._farmerShadows) { if (!Game1.multiplayer.isDisconnecting(farmerShadow.UniqueMultiplayerID) && !(bool)((NetFieldBase)farmerShadow.swimming) && !farmerShadow.isRidingHorse() && (Game1.currentLocation == null || !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(farmerShadow.getTileLocation()))) - { - SpriteBatch spriteBatch = Game1.spriteBatch; - Texture2D shadowTexture = Game1.shadowTexture; - Vector2 local = Game1.GlobalToLocal(farmerShadow.Position + new Vector2(32f, 24f)); - Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds); - Color white = Color.White; - Microsoft.Xna.Framework.Rectangle bounds = Game1.shadowTexture.Bounds; - double x = (double)bounds.Center.X; - bounds = Game1.shadowTexture.Bounds; - double y = (double)bounds.Center.Y; - Vector2 origin = new Vector2((float)x, (float)y); - double num = 4.0 - (!farmerShadow.running && !farmerShadow.UsingTool || farmerShadow.FarmerSprite.currentAnimationIndex <= 1 ? 0.0 : (double)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[farmerShadow.FarmerSprite.CurrentFrame]) * 0.5); - spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, 0.0f, origin, (float)num, SpriteEffects.None, 0.0f); - } + Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(farmerShadow.Position + new Vector2(32f, 24f)), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Microsoft.Xna.Framework.Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 - (!farmerShadow.running && !farmerShadow.UsingTool || farmerShadow.FarmerSprite.currentAnimationIndex <= 1 ? 0.0 : (double)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[farmerShadow.FarmerSprite.CurrentFrame]) * 0.5)), SpriteEffects.None, 0.0f); } } Layer layer = Game1.currentLocation.Map.GetLayer("Buildings"); @@ -1107,7 +1089,7 @@ namespace StardewModdingAPI.Framework foreach (NPC character in Game1.currentLocation.characters) { if (!(bool)((NetFieldBase)character.swimming) && !character.HideShadow && (!(bool)((NetFieldBase)character.isInvisible) && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(character.getTileLocation()))) - Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, character.Position + new Vector2((float)(character.Sprite.SpriteWidth * 4) / 2f, (float)(character.GetBoundingBox().Height + (character.IsMonster ? 0 : 12)))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)character.yJumpOffset / 40.0) * (float)((NetFieldBase)character.scale), SpriteEffects.None, Math.Max(0.0f, (float)character.getStandingY() / 10000f) - 1E-06f); + Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, character.Position + new Vector2((float)(character.Sprite.SpriteWidth * 4) / 2f, (float)(character.GetBoundingBox().Height + (character.IsMonster ? 0 : 12)))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Microsoft.Xna.Framework.Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)character.yJumpOffset / 40.0) * (float)((NetFieldBase)character.scale), SpriteEffects.None, Math.Max(0.0f, (float)character.getStandingY() / 10000f) - 1E-06f); } } else @@ -1115,34 +1097,20 @@ namespace StardewModdingAPI.Framework foreach (NPC actor in Game1.CurrentEvent.actors) { if (!(bool)((NetFieldBase)actor.swimming) && !actor.HideShadow && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(actor.getTileLocation())) - Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, actor.Position + new Vector2((float)(actor.Sprite.SpriteWidth * 4) / 2f, (float)(actor.GetBoundingBox().Height + (actor.IsMonster ? 0 : 12)))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)actor.yJumpOffset / 40.0) * (float)((NetFieldBase)actor.scale), SpriteEffects.None, Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 1E-06f); + Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, actor.Position + new Vector2((float)(actor.Sprite.SpriteWidth * 4) / 2f, (float)(actor.GetBoundingBox().Height + (actor.IsMonster ? 0 : 12)))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Microsoft.Xna.Framework.Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)actor.yJumpOffset / 40.0) * (float)((NetFieldBase)actor.scale), SpriteEffects.None, Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 1E-06f); } } foreach (Farmer farmerShadow in this._farmerShadows) { - float num1 = Math.Max(0.0001f, farmerShadow.getDrawLayer() + 0.00011f) - 0.0001f; + float layerDepth = Math.Max(0.0001f, farmerShadow.getDrawLayer() + 0.00011f) - 0.0001f; if (!(bool)((NetFieldBase)farmerShadow.swimming) && !farmerShadow.isRidingHorse() && (Game1.currentLocation != null && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(farmerShadow.getTileLocation()))) - { - SpriteBatch spriteBatch = Game1.spriteBatch; - Texture2D shadowTexture = Game1.shadowTexture; - Vector2 local = Game1.GlobalToLocal(farmerShadow.Position + new Vector2(32f, 24f)); - Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds); - Color white = Color.White; - Microsoft.Xna.Framework.Rectangle bounds = Game1.shadowTexture.Bounds; - double x = (double)bounds.Center.X; - bounds = Game1.shadowTexture.Bounds; - double y = (double)bounds.Center.Y; - Vector2 origin = new Vector2((float)x, (float)y); - double num2 = 4.0 - (!farmerShadow.running && !farmerShadow.UsingTool || farmerShadow.FarmerSprite.currentAnimationIndex <= 1 ? 0.0 : (double)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[farmerShadow.FarmerSprite.CurrentFrame]) * 0.5); - double num3 = (double)num1; - spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, 0.0f, origin, (float)num2, SpriteEffects.None, (float)num3); - } + Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(farmerShadow.Position + new Vector2(32f, 24f)), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Microsoft.Xna.Framework.Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 - (!farmerShadow.running && !farmerShadow.UsingTool || farmerShadow.FarmerSprite.currentAnimationIndex <= 1 ? 0.0 : (double)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[farmerShadow.FarmerSprite.CurrentFrame]) * 0.5)), SpriteEffects.None, layerDepth); } } if ((Game1.eventUp || Game1.killScreen) && (!Game1.killScreen && Game1.currentLocation.currentEvent != null)) Game1.currentLocation.currentEvent.draw(Game1.spriteBatch); if (Game1.player.currentUpgrade != null && Game1.player.currentUpgrade.daysLeftTillUpgradeDone <= 3 && Game1.currentLocation.Name.Equals("Farm")) - Game1.spriteBatch.Draw(Game1.player.currentUpgrade.workerTexture, Game1.GlobalToLocal(Game1.viewport, Game1.player.currentUpgrade.positionOfCarpenter), new Microsoft.Xna.Framework.Rectangle?(Game1.player.currentUpgrade.getSourceRectangle()), Color.White, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, (float)(((double)Game1.player.currentUpgrade.positionOfCarpenter.Y + 48.0) / 10000.0)); + Game1.spriteBatch.Draw(Game1.player.currentUpgrade.workerTexture, Game1.GlobalToLocal(Game1.viewport, Game1.player.currentUpgrade.positionOfCarpenter), new Microsoft.Xna.Framework.Rectangle?(Game1.player.currentUpgrade.getSourceRectangle()), Microsoft.Xna.Framework.Color.White, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, (float)(((double)Game1.player.currentUpgrade.positionOfCarpenter.Y + 48.0) / 10000.0)); Game1.currentLocation.draw(Game1.spriteBatch); foreach (Vector2 key in Game1.crabPotOverlayTiles.Keys) { @@ -1163,12 +1131,12 @@ namespace StardewModdingAPI.Framework if (Game1.currentLocation.Name.Equals("Farm")) this.drawFarmBuildings(); if (Game1.tvStation >= 0) - Game1.spriteBatch.Draw(Game1.tvStationTexture, Game1.GlobalToLocal(Game1.viewport, new Vector2(400f, 160f)), new Microsoft.Xna.Framework.Rectangle?(new Microsoft.Xna.Framework.Rectangle(Game1.tvStation * 24, 0, 24, 15)), Color.White, 0.0f, Vector2.Zero, 4f, SpriteEffects.None, 1E-08f); + Game1.spriteBatch.Draw(Game1.tvStationTexture, Game1.GlobalToLocal(Game1.viewport, new Vector2(400f, 160f)), new Microsoft.Xna.Framework.Rectangle?(new Microsoft.Xna.Framework.Rectangle(Game1.tvStation * 24, 0, 24, 15)), Microsoft.Xna.Framework.Color.White, 0.0f, Vector2.Zero, 4f, SpriteEffects.None, 1E-08f); if (Game1.panMode) { - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle((int)Math.Floor((double)(Game1.getOldMouseX() + Game1.viewport.X) / 64.0) * 64 - Game1.viewport.X, (int)Math.Floor((double)(Game1.getOldMouseY() + Game1.viewport.Y) / 64.0) * 64 - Game1.viewport.Y, 64, 64), Color.Lime * 0.75f); + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle((int)Math.Floor((double)(Game1.getOldMouseX() + Game1.viewport.X) / 64.0) * 64 - Game1.viewport.X, (int)Math.Floor((double)(Game1.getOldMouseY() + Game1.viewport.Y) / 64.0) * 64 - Game1.viewport.Y, 64, 64), Microsoft.Xna.Framework.Color.Lime * 0.75f); foreach (Warp warp in (NetList>)Game1.currentLocation.warps) - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle(warp.X * 64 - Game1.viewport.X, warp.Y * 64 - Game1.viewport.Y, 64, 64), Color.Red * 0.75f); + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle(warp.X * 64 - Game1.viewport.X, warp.Y * 64 - Game1.viewport.Y, 64, 64), Microsoft.Xna.Framework.Color.Red * 0.75f); } Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); Game1.currentLocation.Map.GetLayer("Front").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, 4); @@ -1190,7 +1158,7 @@ namespace StardewModdingAPI.Framework } if ((double)Game1.toolHold > 400.0 && Game1.player.CurrentTool.UpgradeLevel >= 1 && Game1.player.canReleaseTool) { - Color color = Color.White; + Microsoft.Xna.Framework.Color color = Microsoft.Xna.Framework.Color.White; switch ((int)((double)Game1.toolHold / 600.0) + 2) { case 1: @@ -1206,7 +1174,7 @@ namespace StardewModdingAPI.Framework color = Tool.iridiumColor; break; } - Game1.spriteBatch.Draw(Game1.littleEffect, new Microsoft.Xna.Framework.Rectangle((int)Game1.player.getLocalPosition(Game1.viewport).X - 2, (int)Game1.player.getLocalPosition(Game1.viewport).Y - (Game1.player.CurrentTool.Name.Equals("Watering Can") ? 0 : 64) - 2, (int)((double)Game1.toolHold % 600.0 * 0.0799999982118607) + 4, 12), Color.Black); + Game1.spriteBatch.Draw(Game1.littleEffect, new Microsoft.Xna.Framework.Rectangle((int)Game1.player.getLocalPosition(Game1.viewport).X - 2, (int)Game1.player.getLocalPosition(Game1.viewport).Y - (Game1.player.CurrentTool.Name.Equals("Watering Can") ? 0 : 64) - 2, (int)((double)Game1.toolHold % 600.0 * 0.0799999982118607) + 4, 12), Microsoft.Xna.Framework.Color.Black); Game1.spriteBatch.Draw(Game1.littleEffect, new Microsoft.Xna.Framework.Rectangle((int)Game1.player.getLocalPosition(Game1.viewport).X, (int)Game1.player.getLocalPosition(Game1.viewport).Y - (Game1.player.CurrentTool.Name.Equals("Watering Can") ? 0 : 64), (int)((double)Game1.toolHold % 600.0 * 0.0799999982118607), 8), color); } bool flag = Game1.viewport.X > -Game1.viewport.Width; @@ -1223,7 +1191,7 @@ namespace StardewModdingAPI.Framework Texture2D fadeToBlackRect = Game1.fadeToBlackRect; viewport = Game1.graphics.GraphicsDevice.Viewport; Microsoft.Xna.Framework.Rectangle bounds = viewport.Bounds; - Color color = Color.Black * Game1.currentLocation.LightLevel; + Microsoft.Xna.Framework.Color color = Microsoft.Xna.Framework.Color.Black * Game1.currentLocation.LightLevel; spriteBatch.Draw(fadeToBlackRect, bounds, color); } if (Game1.screenGlow) @@ -1232,7 +1200,7 @@ namespace StardewModdingAPI.Framework Texture2D fadeToBlackRect = Game1.fadeToBlackRect; viewport = Game1.graphics.GraphicsDevice.Viewport; Microsoft.Xna.Framework.Rectangle bounds = viewport.Bounds; - Color color = Game1.screenGlowColor * Game1.screenGlowAlpha; + Microsoft.Xna.Framework.Color color = Game1.screenGlowColor * Game1.screenGlowAlpha; spriteBatch.Draw(fadeToBlackRect, bounds, color); } Game1.currentLocation.drawAboveAlwaysFrontLayer(Game1.spriteBatch); @@ -1241,7 +1209,7 @@ namespace StardewModdingAPI.Framework if (Game1.isRaining && Game1.currentLocation.IsOutdoors && (!Game1.currentLocation.Name.Equals("Desert") && !(Game1.currentLocation is Summit)) && (!Game1.eventUp || Game1.currentLocation.isTileOnMap(new Vector2((float)(Game1.viewport.X / 64), (float)(Game1.viewport.Y / 64))))) { for (int index = 0; index < Game1.rainDrops.Length; ++index) - Game1.spriteBatch.Draw(Game1.rainTexture, Game1.rainDrops[index].position, new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.rainTexture, Game1.rainDrops[index].frame, -1, -1)), Color.White); + Game1.spriteBatch.Draw(Game1.rainTexture, Game1.rainDrops[index].position, new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.rainTexture, Game1.rainDrops[index].frame, -1, -1)), Microsoft.Xna.Framework.Color.White); } Game1.spriteBatch.End(); Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); @@ -1257,7 +1225,7 @@ namespace StardewModdingAPI.Framework localPosition.Y += 32f; else if (actor.Gender == 1) localPosition.Y += 10f; - Game1.spriteBatch.Draw(Game1.emoteSpriteSheet, localPosition, new Microsoft.Xna.Framework.Rectangle?(new Microsoft.Xna.Framework.Rectangle(actor.CurrentEmoteIndex * 16 % Game1.emoteSpriteSheet.Width, actor.CurrentEmoteIndex * 16 / Game1.emoteSpriteSheet.Width * 16, 16, 16)), Color.White, 0.0f, Vector2.Zero, 4f, SpriteEffects.None, (float)actor.getStandingY() / 10000f); + Game1.spriteBatch.Draw(Game1.emoteSpriteSheet, localPosition, new Microsoft.Xna.Framework.Rectangle?(new Microsoft.Xna.Framework.Rectangle(actor.CurrentEmoteIndex * 16 % Game1.emoteSpriteSheet.Width, actor.CurrentEmoteIndex * 16 / Game1.emoteSpriteSheet.Width * 16, 16, 16)), Microsoft.Xna.Framework.Color.White, 0.0f, Vector2.Zero, 4f, SpriteEffects.None, (float)actor.getStandingY() / 10000f); } } } @@ -1265,14 +1233,14 @@ namespace StardewModdingAPI.Framework if (Game1.drawLighting) { Game1.spriteBatch.Begin(SpriteSortMode.Deferred, this.lightingBlend, SamplerState.LinearClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.spriteBatch.Draw((Texture2D)Game1.lightmap, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(Game1.lightmap.Bounds), Color.White, 0.0f, Vector2.Zero, (float)(Game1.options.lightingQuality / 2), SpriteEffects.None, 1f); + Game1.spriteBatch.Draw((Texture2D)Game1.lightmap, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(Game1.lightmap.Bounds), Microsoft.Xna.Framework.Color.White, 0.0f, Vector2.Zero, (float)(Game1.options.lightingQuality / 2), SpriteEffects.None, 1f); if (Game1.isRaining && (bool)((NetFieldBase)Game1.currentLocation.isOutdoors) && !(Game1.currentLocation is Desert)) { SpriteBatch spriteBatch = Game1.spriteBatch; Texture2D staminaRect = Game1.staminaRect; viewport = Game1.graphics.GraphicsDevice.Viewport; Microsoft.Xna.Framework.Rectangle bounds = viewport.Bounds; - Color color = Color.OrangeRed * 0.45f; + Microsoft.Xna.Framework.Color color = Microsoft.Xna.Framework.Color.OrangeRed * 0.45f; spriteBatch.Draw(staminaRect, bounds, color); } Game1.spriteBatch.End(); @@ -1298,7 +1266,7 @@ namespace StardewModdingAPI.Framework viewport = Game1.graphics.GraphicsDevice.Viewport; int height = viewport.Height; Microsoft.Xna.Framework.Rectangle destinationRectangle = new Microsoft.Xna.Framework.Rectangle(x, y, 1, height); - Color color = Color.Red * 0.5f; + Microsoft.Xna.Framework.Color color = Microsoft.Xna.Framework.Color.Red * 0.5f; spriteBatch.Draw(staminaRect, destinationRectangle, color); num3 += 64; } @@ -1320,7 +1288,7 @@ namespace StardewModdingAPI.Framework viewport = Game1.graphics.GraphicsDevice.Viewport; int width = viewport.Width; Microsoft.Xna.Framework.Rectangle destinationRectangle = new Microsoft.Xna.Framework.Rectangle(x, y, width, 1); - Color color = Color.Red * 0.5f; + Microsoft.Xna.Framework.Color color = Microsoft.Xna.Framework.Color.Red * 0.5f; spriteBatch.Draw(staminaRect, destinationRectangle, color); num5 += 64f; } @@ -1328,9 +1296,9 @@ namespace StardewModdingAPI.Framework break; } } - if (Game1.currentBillboard != 0) + if (Game1.currentBillboard != 0 && !this.takingMapScreenshot) this.drawBillboard(); - if (!Game1.eventUp && Game1.farmEvent == null && (Game1.currentBillboard == 0 && Game1.gameMode == (byte)3) && Game1.isOutdoorMapSmallerThanViewport()) + if (!Game1.eventUp && Game1.farmEvent == null && (Game1.currentBillboard == 0 && Game1.gameMode == (byte)3) && (!this.takingMapScreenshot && Game1.isOutdoorMapSmallerThanViewport())) { SpriteBatch spriteBatch1 = Game1.spriteBatch; Texture2D fadeToBlackRect1 = Game1.fadeToBlackRect; @@ -1338,7 +1306,7 @@ namespace StardewModdingAPI.Framework viewport = Game1.graphics.GraphicsDevice.Viewport; int height1 = viewport.Height; Microsoft.Xna.Framework.Rectangle destinationRectangle1 = new Microsoft.Xna.Framework.Rectangle(0, 0, width1, height1); - Color black1 = Color.Black; + Microsoft.Xna.Framework.Color black1 = Microsoft.Xna.Framework.Color.Black; spriteBatch1.Draw(fadeToBlackRect1, destinationRectangle1, black1); SpriteBatch spriteBatch2 = Game1.spriteBatch; Texture2D fadeToBlackRect2 = Game1.fadeToBlackRect; @@ -1348,10 +1316,10 @@ namespace StardewModdingAPI.Framework viewport = Game1.graphics.GraphicsDevice.Viewport; int height2 = viewport.Height; Microsoft.Xna.Framework.Rectangle destinationRectangle2 = new Microsoft.Xna.Framework.Rectangle(x, 0, width2, height2); - Color black2 = Color.Black; + Microsoft.Xna.Framework.Color black2 = Microsoft.Xna.Framework.Color.Black; spriteBatch2.Draw(fadeToBlackRect2, destinationRectangle2, black2); } - if ((Game1.displayHUD || Game1.eventUp) && (Game1.currentBillboard == 0 && Game1.gameMode == (byte)3) && (!Game1.freezeControls && !Game1.panMode && !Game1.HostPaused)) + if ((Game1.displayHUD || Game1.eventUp) && (Game1.currentBillboard == 0 && Game1.gameMode == (byte)3) && (!Game1.freezeControls && !Game1.panMode && (!Game1.HostPaused && !this.takingMapScreenshot))) { events.RenderingHud.RaiseEmpty(); this.drawHUD(); @@ -1361,7 +1329,7 @@ namespace StardewModdingAPI.Framework { FarmEvent farmEvent = Game1.farmEvent; } - if (Game1.hudMessages.Count > 0) + if (Game1.hudMessages.Count > 0 && !this.takingMapScreenshot) { for (int i = Game1.hudMessages.Count - 1; i >= 0; --i) Game1.hudMessages[i].draw(Game1.spriteBatch, i); @@ -1369,12 +1337,12 @@ namespace StardewModdingAPI.Framework } if (Game1.farmEvent != null) Game1.farmEvent.draw(Game1.spriteBatch); - if (Game1.dialogueUp && !Game1.nameSelectUp && !Game1.messagePause && (Game1.activeClickableMenu == null || !(Game1.activeClickableMenu is DialogueBox))) + if (Game1.dialogueUp && !Game1.nameSelectUp && !Game1.messagePause && ((Game1.activeClickableMenu == null || !(Game1.activeClickableMenu is DialogueBox)) && !this.takingMapScreenshot)) this.drawDialogueBox(); - if (Game1.progressBar) + if (Game1.progressBar && !this.takingMapScreenshot) { - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle((Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Width - Game1.dialogueWidth) / 2, Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Bottom - 128, Game1.dialogueWidth, 32), Color.LightGray); - Game1.spriteBatch.Draw(Game1.staminaRect, new Microsoft.Xna.Framework.Rectangle((Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Width - Game1.dialogueWidth) / 2, Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Bottom - 128, (int)((double)Game1.pauseAccumulator / (double)Game1.pauseTime * (double)Game1.dialogueWidth), 32), Color.DimGray); + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle((Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Width - Game1.dialogueWidth) / 2, Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Bottom - 128, Game1.dialogueWidth, 32), Microsoft.Xna.Framework.Color.LightGray); + Game1.spriteBatch.Draw(Game1.staminaRect, new Microsoft.Xna.Framework.Rectangle((Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Width - Game1.dialogueWidth) / 2, Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Bottom - 128, (int)((double)Game1.pauseAccumulator / (double)Game1.pauseTime * (double)Game1.dialogueWidth), 32), Microsoft.Xna.Framework.Color.DimGray); } if (Game1.eventUp && Game1.currentLocation != null && Game1.currentLocation.currentEvent != null) Game1.currentLocation.currentEvent.drawAfterMap(Game1.spriteBatch); @@ -1384,19 +1352,19 @@ namespace StardewModdingAPI.Framework Texture2D staminaRect = Game1.staminaRect; viewport = Game1.graphics.GraphicsDevice.Viewport; Microsoft.Xna.Framework.Rectangle bounds = viewport.Bounds; - Color color = Color.Blue * 0.2f; + Microsoft.Xna.Framework.Color color = Microsoft.Xna.Framework.Color.Blue * 0.2f; spriteBatch.Draw(staminaRect, bounds, color); } - if ((Game1.fadeToBlack || Game1.globalFade) && !Game1.menuUp && (!Game1.nameSelectUp || Game1.messagePause)) + if ((Game1.fadeToBlack || Game1.globalFade) && !Game1.menuUp && ((!Game1.nameSelectUp || Game1.messagePause) && !this.takingMapScreenshot)) { SpriteBatch spriteBatch = Game1.spriteBatch; Texture2D fadeToBlackRect = Game1.fadeToBlackRect; viewport = Game1.graphics.GraphicsDevice.Viewport; Microsoft.Xna.Framework.Rectangle bounds = viewport.Bounds; - Color color = Color.Black * (Game1.gameMode == (byte)0 ? 1f - Game1.fadeToBlackAlpha : Game1.fadeToBlackAlpha); + Microsoft.Xna.Framework.Color color = Microsoft.Xna.Framework.Color.Black * (Game1.gameMode == (byte)0 ? 1f - Game1.fadeToBlackAlpha : Game1.fadeToBlackAlpha); spriteBatch.Draw(fadeToBlackRect, bounds, color); } - else if ((double)Game1.flashAlpha > 0.0) + else if ((double)Game1.flashAlpha > 0.0 && !this.takingMapScreenshot) { if (Game1.options.screenFlash) { @@ -1404,15 +1372,18 @@ namespace StardewModdingAPI.Framework Texture2D fadeToBlackRect = Game1.fadeToBlackRect; viewport = Game1.graphics.GraphicsDevice.Viewport; Microsoft.Xna.Framework.Rectangle bounds = viewport.Bounds; - Color color = Color.White * Math.Min(1f, Game1.flashAlpha); + Microsoft.Xna.Framework.Color color = Microsoft.Xna.Framework.Color.White * Math.Min(1f, Game1.flashAlpha); spriteBatch.Draw(fadeToBlackRect, bounds, color); } Game1.flashAlpha -= 0.1f; } - if ((Game1.messagePause || Game1.globalFade) && Game1.dialogueUp) + if ((Game1.messagePause || Game1.globalFade) && (Game1.dialogueUp && !this.takingMapScreenshot)) this.drawDialogueBox(); - foreach (TemporaryAnimatedSprite overlayTempSprite in Game1.screenOverlayTempSprites) - overlayTempSprite.draw(Game1.spriteBatch, true, 0, 0, 1f); + if (!this.takingMapScreenshot) + { + foreach (TemporaryAnimatedSprite overlayTempSprite in Game1.screenOverlayTempSprites) + overlayTempSprite.draw(Game1.spriteBatch, true, 0, 0, 1f); + } if (Game1.debugMode) { StringBuilder debugStringBuilder = Game1._debugStringBuilder; @@ -1443,11 +1414,11 @@ namespace StardewModdingAPI.Framework debugStringBuilder.Append(Game1.getMouseY() + Game1.viewport.Y); debugStringBuilder.Append(" debugOutput: "); debugStringBuilder.Append(Game1.debugOutput); - Game1.spriteBatch.DrawString(Game1.smallFont, debugStringBuilder, new Vector2((float)this.GraphicsDevice.Viewport.GetTitleSafeArea().X, (float)(this.GraphicsDevice.Viewport.GetTitleSafeArea().Y + Game1.smallFont.LineSpacing * 8)), Color.Red, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, 0.9999999f); + Game1.spriteBatch.DrawString(Game1.smallFont, debugStringBuilder, new Vector2((float)this.GraphicsDevice.Viewport.GetTitleSafeArea().X, (float)(this.GraphicsDevice.Viewport.GetTitleSafeArea().Y + Game1.smallFont.LineSpacing * 8)), Microsoft.Xna.Framework.Color.Red, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, 0.9999999f); } - if (Game1.showKeyHelp) - Game1.spriteBatch.DrawString(Game1.smallFont, Game1.keyHelpString, new Vector2(64f, (float)(Game1.viewport.Height - 64 - (Game1.dialogueUp ? 192 + (Game1.isQuestion ? Game1.questionChoices.Count * 64 : 0) : 0)) - Game1.smallFont.MeasureString(Game1.keyHelpString).Y), Color.LightGray, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, 0.9999999f); - if (Game1.activeClickableMenu != null) + if (Game1.showKeyHelp && !this.takingMapScreenshot) + Game1.spriteBatch.DrawString(Game1.smallFont, Game1.keyHelpString, new Vector2(64f, (float)(Game1.viewport.Height - 64 - (Game1.dialogueUp ? 192 + (Game1.isQuestion ? Game1.questionChoices.Count * 64 : 0) : 0)) - Game1.smallFont.MeasureString(Game1.keyHelpString).Y), Microsoft.Xna.Framework.Color.LightGray, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, 0.9999999f); + if (Game1.activeClickableMenu != null && !this.takingMapScreenshot) { try { @@ -1463,9 +1434,9 @@ namespace StardewModdingAPI.Framework } else if (Game1.farmEvent != null) Game1.farmEvent.drawAboveEverything(Game1.spriteBatch); - if (Game1.emoteMenu != null) + if (Game1.emoteMenu != null && !this.takingMapScreenshot) Game1.emoteMenu.draw(Game1.spriteBatch); - if (Game1.HostPaused) + if (Game1.HostPaused && !this.takingMapScreenshot) { string s = Game1.content.LoadString("Strings\\StringsFromCSFiles:DayTimeMoneyBox.cs.10378"); SpriteText.drawStringWithScrollBackground(Game1.spriteBatch, s, 96, 32, "", 1f, -1, SpriteText.ScrollTextAlignment.Left); -- cgit From 6e46990ed116e55e95de58047753b4fe1c3dcaf6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 15 Aug 2019 22:26:14 -0400 Subject: update for more draw changes (#638) --- src/SMAPI/Framework/SGame.cs | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index cdd7437a..0d518564 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -857,7 +857,7 @@ namespace StardewModdingAPI.Framework Game1.overlayMenu.draw(Game1.spriteBatch); Game1.spriteBatch.End(); } - this.renderScreenBuffer(); + this.renderScreenBuffer(target_screen); } else { @@ -1177,11 +1177,26 @@ namespace StardewModdingAPI.Framework Game1.spriteBatch.Draw(Game1.littleEffect, new Microsoft.Xna.Framework.Rectangle((int)Game1.player.getLocalPosition(Game1.viewport).X - 2, (int)Game1.player.getLocalPosition(Game1.viewport).Y - (Game1.player.CurrentTool.Name.Equals("Watering Can") ? 0 : 64) - 2, (int)((double)Game1.toolHold % 600.0 * 0.0799999982118607) + 4, 12), Microsoft.Xna.Framework.Color.Black); Game1.spriteBatch.Draw(Game1.littleEffect, new Microsoft.Xna.Framework.Rectangle((int)Game1.player.getLocalPosition(Game1.viewport).X, (int)Game1.player.getLocalPosition(Game1.viewport).Y - (Game1.player.CurrentTool.Name.Equals("Watering Can") ? 0 : 64), (int)((double)Game1.toolHold % 600.0 * 0.0799999982118607), 8), color); } - bool flag = Game1.viewport.X > -Game1.viewport.Width; - if (((!Game1.isDebrisWeather || !Game1.currentLocation.IsOutdoors || (bool)((NetFieldBase)Game1.currentLocation.ignoreDebrisWeather) ? 0 : (!Game1.currentLocation.Name.Equals("Desert") ? 1 : 0)) & (flag ? 1 : 0)) != 0) + if (Game1.isDebrisWeather && Game1.currentLocation.IsOutdoors && (!(bool)((NetFieldBase)Game1.currentLocation.ignoreDebrisWeather) && !Game1.currentLocation.Name.Equals("Desert"))) { - foreach (WeatherDebris weatherDebris in Game1.debrisWeather) - weatherDebris.draw(Game1.spriteBatch); + if (this.takingMapScreenshot) + { + if (Game1.debrisWeather != null) + { + foreach (WeatherDebris weatherDebris in Game1.debrisWeather) + { + Vector2 position = weatherDebris.position; + weatherDebris.position = new Vector2((float)Game1.random.Next(Game1.viewport.Width - weatherDebris.sourceRect.Width * 3), (float)Game1.random.Next(Game1.viewport.Height - weatherDebris.sourceRect.Height * 3)); + weatherDebris.draw(Game1.spriteBatch); + weatherDebris.position = position; + } + } + } + else if (Game1.viewport.X > -Game1.viewport.Width) + { + foreach (WeatherDebris weatherDebris in Game1.debrisWeather) + weatherDebris.draw(Game1.spriteBatch); + } } if (Game1.farmEvent != null) Game1.farmEvent.draw(Game1.spriteBatch); @@ -1445,7 +1460,7 @@ namespace StardewModdingAPI.Framework events.Rendered.RaiseEmpty(); Game1.spriteBatch.End(); this.drawOverlays(Game1.spriteBatch); - this.renderScreenBuffer(); + this.renderScreenBuffer(target_screen); } } } -- cgit From 02300fcaa8cb434d56f4a31f071b33f0003e0b88 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 10 Sep 2019 20:53:48 -0400 Subject: update for more draw changes (#638) --- src/SMAPI/Framework/SGame.cs | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 0d518564..07b06bab 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -1012,14 +1012,17 @@ namespace StardewModdingAPI.Framework for (int index = 0; index < Game1.currentLightSources.Count; ++index) { LightSource lightSource = Game1.currentLightSources.ElementAt(index); - if (lightSource.PlayerID != 0L && lightSource.PlayerID != Game1.player.UniqueMultiplayerID) + if ((!Game1.isDarkOut() || lightSource.lightContext.Value != LightSource.LightContext.WindowLight) && !Game1.isRaining) { - Farmer farmerMaybeOffline = Game1.getFarmerMaybeOffline(lightSource.PlayerID); - if (farmerMaybeOffline == null || farmerMaybeOffline.currentLocation != null && farmerMaybeOffline.currentLocation.Name != Game1.currentLocation.Name || (bool)((NetFieldBase)farmerMaybeOffline.hidden)) - continue; + if (lightSource.PlayerID != 0L && lightSource.PlayerID != Game1.player.UniqueMultiplayerID) + { + Farmer farmerMaybeOffline = Game1.getFarmerMaybeOffline(lightSource.PlayerID); + if (farmerMaybeOffline == null || farmerMaybeOffline.currentLocation != null && farmerMaybeOffline.currentLocation.Name != Game1.currentLocation.Name || (bool)((NetFieldBase)farmerMaybeOffline.hidden)) + continue; + } + if (Utility.isOnScreen((Vector2)((NetFieldBase)Game1.currentLightSources.ElementAt(index).position), (int)((double)(float)((NetFieldBase)Game1.currentLightSources.ElementAt(index).radius) * 64.0 * 4.0))) + Game1.spriteBatch.Draw(Game1.currentLightSources.ElementAt(index).lightTexture, Game1.GlobalToLocal(Game1.viewport, (Vector2)((NetFieldBase)Game1.currentLightSources.ElementAt(index).position)) / (float)(Game1.options.lightingQuality / 2), new Microsoft.Xna.Framework.Rectangle?(Game1.currentLightSources.ElementAt(index).lightTexture.Bounds), (Microsoft.Xna.Framework.Color)((NetFieldBase)Game1.currentLightSources.ElementAt(index).color), 0.0f, new Vector2((float)Game1.currentLightSources.ElementAt(index).lightTexture.Bounds.Center.X, (float)Game1.currentLightSources.ElementAt(index).lightTexture.Bounds.Center.Y), (float)((NetFieldBase)Game1.currentLightSources.ElementAt(index).radius) / (float)(Game1.options.lightingQuality / 2), SpriteEffects.None, 0.9f); } - if (Utility.isOnScreen((Vector2)((NetFieldBase)Game1.currentLightSources.ElementAt(index).position), (int)((double)(float)((NetFieldBase)Game1.currentLightSources.ElementAt(index).radius) * 64.0 * 4.0))) - Game1.spriteBatch.Draw(Game1.currentLightSources.ElementAt(index).lightTexture, Game1.GlobalToLocal(Game1.viewport, (Vector2)((NetFieldBase)Game1.currentLightSources.ElementAt(index).position)) / (float)(Game1.options.lightingQuality / 2), new Microsoft.Xna.Framework.Rectangle?(Game1.currentLightSources.ElementAt(index).lightTexture.Bounds), (Microsoft.Xna.Framework.Color)((NetFieldBase)Game1.currentLightSources.ElementAt(index).color), 0.0f, new Vector2((float)Game1.currentLightSources.ElementAt(index).lightTexture.Bounds.Center.X, (float)Game1.currentLightSources.ElementAt(index).lightTexture.Bounds.Center.Y), (float)((NetFieldBase)Game1.currentLightSources.ElementAt(index).radius) / (float)(Game1.options.lightingQuality / 2), SpriteEffects.None, 0.9f); } Game1.spriteBatch.End(); this.GraphicsDevice.SetRenderTarget(target_screen); @@ -1221,10 +1224,21 @@ namespace StardewModdingAPI.Framework Game1.currentLocation.drawAboveAlwaysFrontLayer(Game1.spriteBatch); if (Game1.player.CurrentTool != null && Game1.player.CurrentTool is FishingRod && ((Game1.player.CurrentTool as FishingRod).isTimingCast || (double)(Game1.player.CurrentTool as FishingRod).castingChosenCountdown > 0.0 || ((Game1.player.CurrentTool as FishingRod).fishCaught || (Game1.player.CurrentTool as FishingRod).showingTreasure))) Game1.player.CurrentTool.draw(Game1.spriteBatch); - if (Game1.isRaining && Game1.currentLocation.IsOutdoors && (!Game1.currentLocation.Name.Equals("Desert") && !(Game1.currentLocation is Summit)) && (!Game1.eventUp || Game1.currentLocation.isTileOnMap(new Vector2((float)(Game1.viewport.X / 64), (float)(Game1.viewport.Y / 64))))) + if (Game1.isRaining && Game1.currentLocation.IsOutdoors && (!Game1.currentLocation.Name.Equals("Desert") && !(Game1.currentLocation is Summit))) { - for (int index = 0; index < Game1.rainDrops.Length; ++index) - Game1.spriteBatch.Draw(Game1.rainTexture, Game1.rainDrops[index].position, new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.rainTexture, Game1.rainDrops[index].frame, -1, -1)), Microsoft.Xna.Framework.Color.White); + if (this.takingMapScreenshot) + { + for (int index = 0; index < Game1.rainDrops.Length; ++index) + { + Vector2 position = new Vector2((float)Game1.random.Next(Game1.viewport.Width - 64), (float)Game1.random.Next(Game1.viewport.Height - 64)); + Game1.spriteBatch.Draw(Game1.rainTexture, position, new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.rainTexture, Game1.rainDrops[index].frame, -1, -1)), Microsoft.Xna.Framework.Color.White); + } + } + else if (!Game1.eventUp || Game1.currentLocation.isTileOnMap(new Vector2((float)(Game1.viewport.X / 64), (float)(Game1.viewport.Y / 64)))) + { + for (int index = 0; index < Game1.rainDrops.Length; ++index) + Game1.spriteBatch.Draw(Game1.rainTexture, Game1.rainDrops[index].position, new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.rainTexture, Game1.rainDrops[index].frame, -1, -1)), Microsoft.Xna.Framework.Color.White); + } } Game1.spriteBatch.End(); Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); -- cgit From 2717ea8cb169767286bf8c24ac2178f2316e893c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 12 Sep 2019 23:56:45 -0400 Subject: update for more draw changes (#638) --- src/SMAPI/Framework/SGame.cs | 42 +++--------------------------------------- 1 file changed, 3 insertions(+), 39 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 07b06bab..cabbbc3a 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -1012,7 +1012,7 @@ namespace StardewModdingAPI.Framework for (int index = 0; index < Game1.currentLightSources.Count; ++index) { LightSource lightSource = Game1.currentLightSources.ElementAt(index); - if ((!Game1.isDarkOut() || lightSource.lightContext.Value != LightSource.LightContext.WindowLight) && !Game1.isRaining) + if (!Game1.isRaining && !Game1.isDarkOut() || lightSource.lightContext.Value != LightSource.LightContext.WindowLight) { if (lightSource.PlayerID != 0L && lightSource.PlayerID != Game1.player.UniqueMultiplayerID) { @@ -1021,7 +1021,7 @@ namespace StardewModdingAPI.Framework continue; } if (Utility.isOnScreen((Vector2)((NetFieldBase)Game1.currentLightSources.ElementAt(index).position), (int)((double)(float)((NetFieldBase)Game1.currentLightSources.ElementAt(index).radius) * 64.0 * 4.0))) - Game1.spriteBatch.Draw(Game1.currentLightSources.ElementAt(index).lightTexture, Game1.GlobalToLocal(Game1.viewport, (Vector2)((NetFieldBase)Game1.currentLightSources.ElementAt(index).position)) / (float)(Game1.options.lightingQuality / 2), new Microsoft.Xna.Framework.Rectangle?(Game1.currentLightSources.ElementAt(index).lightTexture.Bounds), (Microsoft.Xna.Framework.Color)((NetFieldBase)Game1.currentLightSources.ElementAt(index).color), 0.0f, new Vector2((float)Game1.currentLightSources.ElementAt(index).lightTexture.Bounds.Center.X, (float)Game1.currentLightSources.ElementAt(index).lightTexture.Bounds.Center.Y), (float)((NetFieldBase)Game1.currentLightSources.ElementAt(index).radius) / (float)(Game1.options.lightingQuality / 2), SpriteEffects.None, 0.9f); + Game1.spriteBatch.Draw(Game1.currentLightSources.ElementAt(index).lightTexture, Game1.GlobalToLocal(Game1.viewport, (Vector2)((NetFieldBase)Game1.currentLightSources.ElementAt(index).position)) / (float)(Game1.options.lightingQuality / 2), new Microsoft.Xna.Framework.Rectangle?(Game1.currentLightSources.ElementAt(index).lightTexture.Bounds), (Microsoft.Xna.Framework.Color) ((NetFieldBase)Game1.currentLightSources.ElementAt(index).color), 0.0f, new Vector2((float)Game1.currentLightSources.ElementAt(index).lightTexture.Bounds.Center.X, (float)Game1.currentLightSources.ElementAt(index).lightTexture.Bounds.Center.Y), (float)((NetFieldBase)Game1.currentLightSources.ElementAt(index).radius) / (float)(Game1.options.lightingQuality / 2), SpriteEffects.None, 0.9f); } } Game1.spriteBatch.End(); @@ -1180,27 +1180,7 @@ namespace StardewModdingAPI.Framework Game1.spriteBatch.Draw(Game1.littleEffect, new Microsoft.Xna.Framework.Rectangle((int)Game1.player.getLocalPosition(Game1.viewport).X - 2, (int)Game1.player.getLocalPosition(Game1.viewport).Y - (Game1.player.CurrentTool.Name.Equals("Watering Can") ? 0 : 64) - 2, (int)((double)Game1.toolHold % 600.0 * 0.0799999982118607) + 4, 12), Microsoft.Xna.Framework.Color.Black); Game1.spriteBatch.Draw(Game1.littleEffect, new Microsoft.Xna.Framework.Rectangle((int)Game1.player.getLocalPosition(Game1.viewport).X, (int)Game1.player.getLocalPosition(Game1.viewport).Y - (Game1.player.CurrentTool.Name.Equals("Watering Can") ? 0 : 64), (int)((double)Game1.toolHold % 600.0 * 0.0799999982118607), 8), color); } - if (Game1.isDebrisWeather && Game1.currentLocation.IsOutdoors && (!(bool)((NetFieldBase)Game1.currentLocation.ignoreDebrisWeather) && !Game1.currentLocation.Name.Equals("Desert"))) - { - if (this.takingMapScreenshot) - { - if (Game1.debrisWeather != null) - { - foreach (WeatherDebris weatherDebris in Game1.debrisWeather) - { - Vector2 position = weatherDebris.position; - weatherDebris.position = new Vector2((float)Game1.random.Next(Game1.viewport.Width - weatherDebris.sourceRect.Width * 3), (float)Game1.random.Next(Game1.viewport.Height - weatherDebris.sourceRect.Height * 3)); - weatherDebris.draw(Game1.spriteBatch); - weatherDebris.position = position; - } - } - } - else if (Game1.viewport.X > -Game1.viewport.Width) - { - foreach (WeatherDebris weatherDebris in Game1.debrisWeather) - weatherDebris.draw(Game1.spriteBatch); - } - } + this.drawWeather(gameTime, target_screen); if (Game1.farmEvent != null) Game1.farmEvent.draw(Game1.spriteBatch); if ((double)Game1.currentLocation.LightLevel > 0.0 && Game1.timeOfDay < 2000) @@ -1224,22 +1204,6 @@ namespace StardewModdingAPI.Framework Game1.currentLocation.drawAboveAlwaysFrontLayer(Game1.spriteBatch); if (Game1.player.CurrentTool != null && Game1.player.CurrentTool is FishingRod && ((Game1.player.CurrentTool as FishingRod).isTimingCast || (double)(Game1.player.CurrentTool as FishingRod).castingChosenCountdown > 0.0 || ((Game1.player.CurrentTool as FishingRod).fishCaught || (Game1.player.CurrentTool as FishingRod).showingTreasure))) Game1.player.CurrentTool.draw(Game1.spriteBatch); - if (Game1.isRaining && Game1.currentLocation.IsOutdoors && (!Game1.currentLocation.Name.Equals("Desert") && !(Game1.currentLocation is Summit))) - { - if (this.takingMapScreenshot) - { - for (int index = 0; index < Game1.rainDrops.Length; ++index) - { - Vector2 position = new Vector2((float)Game1.random.Next(Game1.viewport.Width - 64), (float)Game1.random.Next(Game1.viewport.Height - 64)); - Game1.spriteBatch.Draw(Game1.rainTexture, position, new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.rainTexture, Game1.rainDrops[index].frame, -1, -1)), Microsoft.Xna.Framework.Color.White); - } - } - else if (!Game1.eventUp || Game1.currentLocation.isTileOnMap(new Vector2((float)(Game1.viewport.X / 64), (float)(Game1.viewport.Y / 64)))) - { - for (int index = 0; index < Game1.rainDrops.Length; ++index) - Game1.spriteBatch.Draw(Game1.rainTexture, Game1.rainDrops[index].position, new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.rainTexture, Game1.rainDrops[index].frame, -1, -1)), Microsoft.Xna.Framework.Color.White); - } - } Game1.spriteBatch.End(); Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); if (Game1.eventUp && Game1.currentLocation.currentEvent != null) -- cgit From 41a809a2e063eec9c39868768512084e0a14256f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 2 Oct 2019 17:37:14 -0400 Subject: fix render events not raised during minigames --- docs/release-notes.md | 1 + src/SMAPI/Framework/SGame.cs | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 5f643f07..3771ec19 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -123,6 +123,7 @@ For modders: * Fixed issue where, when a mod's `IAssetEditor` uses `asset.ReplaceWith` on a texture asset while playing in non-English, any changes from that point won't affect subsequent cached asset loads. * Fixed asset propagation for NPC portraits resetting any unique portraits (e.g. Maru's hospital portrait) to the default. * Fixed changes to `Data\NPCDispositions` not always propagated correctly to existing NPCs. + * Fixed `Rendering`/`Rendered` events not raised during minigames. * Fixed `LoadStageChanged` event not raising correct flags in some cases when creating a new save. * Fixed `GetApi` without an interface not checking if all mods are loaded. diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index cabbbc3a..aa84295c 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -908,6 +908,14 @@ namespace StardewModdingAPI.Framework } else if (Game1.currentMinigame != null) { + int batchEnds = 0; + + if (events.Rendering.HasListeners()) + { + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + events.Rendering.RaiseEmpty(); + Game1.spriteBatch.End(); + } Game1.currentMinigame.draw(Game1.spriteBatch); if (Game1.globalFade && !Game1.menuUp && (!Game1.nameSelectUp || Game1.messagePause)) { @@ -917,11 +925,21 @@ namespace StardewModdingAPI.Framework } this.drawOverlays(Game1.spriteBatch); if (target_screen == null) + { + if (++batchEnds == 1 && events.Rendered.HasListeners()) + { + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + events.Rendered.RaiseEmpty(); + Game1.spriteBatch.End(); + } return; + } this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); this.GraphicsDevice.Clear(Game1.bgColor); Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); Game1.spriteBatch.Draw((Texture2D)target_screen, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(target_screen.Bounds), Microsoft.Xna.Framework.Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); + if (++batchEnds == 1) + events.Rendered.RaiseEmpty(); Game1.spriteBatch.End(); } else if (Game1.showingEndOfNightStuff) -- cgit From 31db556d02b27aa246ea3a5ae0c94dcdd7a5f7f0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 2 Oct 2019 21:56:59 -0400 Subject: update for more draw changes (#638) --- src/SMAPI/Framework/SGame.cs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index aa84295c..47261862 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -762,12 +762,14 @@ namespace StardewModdingAPI.Framework /// The method called to draw everything to the screen. /// A snapshot of the game timing state. - protected override void Draw(GameTime gameTime) + /// The render target, if any. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "copied from game code as-is")] + protected override void _draw(GameTime gameTime, RenderTarget2D target_screen) { Context.IsInDrawLoop = true; try { - this.DrawImpl(gameTime); + this.DrawImpl(gameTime, target_screen); this.DrawCrashTimer.Reset(); } catch (Exception ex) @@ -801,8 +803,10 @@ namespace StardewModdingAPI.Framework /// Replicate the game's draw logic with some changes for SMAPI. /// A snapshot of the game timing state. + /// The render target, if any. /// This implementation is identical to , except for try..catch around menu draw code, private field references replaced by wrappers, and added events. [SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator", Justification = "copied from game code as-is")] + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "copied from game code as-is")] [SuppressMessage("ReSharper", "LocalVariableHidesMember", Justification = "copied from game code as-is")] [SuppressMessage("ReSharper", "PossibleLossOfFraction", Justification = "copied from game code as-is")] [SuppressMessage("ReSharper", "RedundantArgumentDefaultValue", Justification = "copied from game code as-is")] @@ -811,16 +815,10 @@ namespace StardewModdingAPI.Framework [SuppressMessage("ReSharper", "RedundantTypeArgumentsOfMethod", Justification = "copied from game code as-is")] [SuppressMessage("SMAPI.CommonErrors", "AvoidNetField", Justification = "copied from game code as-is")] [SuppressMessage("SMAPI.CommonErrors", "AvoidImplicitNetFieldCast", Justification = "copied from game code as-is")] - private void DrawImpl(GameTime gameTime) + private void DrawImpl(GameTime gameTime, RenderTarget2D target_screen) { var events = this.Events; - // from Game1.Draw - RenderTarget2D target_screen = (RenderTarget2D)null; - if ((double)Game1.options.zoomLevel != 1.0) - target_screen = this.screen; - - // from Game1._draw if (Game1._newDayTask != null) { this.GraphicsDevice.Clear(Game1.bgColor); @@ -1039,7 +1037,7 @@ namespace StardewModdingAPI.Framework continue; } if (Utility.isOnScreen((Vector2)((NetFieldBase)Game1.currentLightSources.ElementAt(index).position), (int)((double)(float)((NetFieldBase)Game1.currentLightSources.ElementAt(index).radius) * 64.0 * 4.0))) - Game1.spriteBatch.Draw(Game1.currentLightSources.ElementAt(index).lightTexture, Game1.GlobalToLocal(Game1.viewport, (Vector2)((NetFieldBase)Game1.currentLightSources.ElementAt(index).position)) / (float)(Game1.options.lightingQuality / 2), new Microsoft.Xna.Framework.Rectangle?(Game1.currentLightSources.ElementAt(index).lightTexture.Bounds), (Microsoft.Xna.Framework.Color) ((NetFieldBase)Game1.currentLightSources.ElementAt(index).color), 0.0f, new Vector2((float)Game1.currentLightSources.ElementAt(index).lightTexture.Bounds.Center.X, (float)Game1.currentLightSources.ElementAt(index).lightTexture.Bounds.Center.Y), (float)((NetFieldBase)Game1.currentLightSources.ElementAt(index).radius) / (float)(Game1.options.lightingQuality / 2), SpriteEffects.None, 0.9f); + Game1.spriteBatch.Draw(Game1.currentLightSources.ElementAt(index).lightTexture, Game1.GlobalToLocal(Game1.viewport, (Vector2)((NetFieldBase)Game1.currentLightSources.ElementAt(index).position)) / (float)(Game1.options.lightingQuality / 2), new Microsoft.Xna.Framework.Rectangle?(Game1.currentLightSources.ElementAt(index).lightTexture.Bounds), (Microsoft.Xna.Framework.Color)((NetFieldBase)Game1.currentLightSources.ElementAt(index).color), 0.0f, new Vector2((float)Game1.currentLightSources.ElementAt(index).lightTexture.Bounds.Center.X, (float)Game1.currentLightSources.ElementAt(index).lightTexture.Bounds.Center.Y), (float)((NetFieldBase)Game1.currentLightSources.ElementAt(index).radius) / (float)(Game1.options.lightingQuality / 2), SpriteEffects.None, 0.9f); } } Game1.spriteBatch.End(); -- cgit From 3d4276707b1599479aa618a9447e2e2d4abb8647 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 16 Nov 2019 18:32:50 -0500 Subject: update CP schema to allow boolean values for Enabled field --- src/SMAPI.Web/wwwroot/schemas/content-patcher.json | 3 +++ 1 file changed, 3 insertions(+) (limited to 'src') diff --git a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json index b75782fe..61a633cb 100644 --- a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json +++ b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json @@ -132,6 +132,9 @@ { "type": "string", "pattern": "\\{\\{[^{}]+\\}\\}" + }, + { + "type": "boolean" } ], "@errorMessages": { -- cgit From f154c5774d6e6197d63ca284606ce965a920d212 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 18 Nov 2019 21:07:52 -0500 Subject: minor updates (#638) --- src/SMAPI/Framework/SCore.cs | 8 -------- src/SMAPI/Metadata/CoreAssetPropagator.cs | 2 +- 2 files changed, 1 insertion(+), 9 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 77b17c8a..afb82679 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -96,12 +96,6 @@ namespace StardewModdingAPI.Framework new Regex(@"^DebugOutput:\s+(?:added CLOUD|added cricket|dismount tile|Ping|playerPos)", RegexOptions.Compiled | RegexOptions.CultureInvariant) }; - /// Regex patterns which match console error messages to suppress from the console and log. - private readonly Regex[] SuppressConsoleErrorPatterns = - { - new Regex(@"^Error loading schedule data for (?:Bouncer|Dwarf|Gunther|Henchman|Krobus|Marlon|Mister Qi|Sandy|Wizard): .+ ---> System\.IO\.FileNotFoundException", RegexOptions.Compiled | RegexOptions.CultureInvariant) - }; - /// Regex patterns which match console messages to show a more friendly error for. private readonly Tuple[] ReplaceConsolePatterns = { @@ -1321,8 +1315,6 @@ namespace StardewModdingAPI.Framework // ignore suppressed message if (level != LogLevel.Error && this.SuppressConsolePatterns.Any(p => p.IsMatch(message))) return; - if (level == LogLevel.Error && this.SuppressConsoleErrorPatterns.Any(p => p.IsMatch(message))) - return; // show friendly error if applicable foreach (var entry in this.ReplaceConsolePatterns) diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index a6bfab5c..9bb9a857 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -735,7 +735,7 @@ namespace StardewModdingAPI.Metadata { Lazy texture = new Lazy(() => content.Load(key)); foreach (Tree tree in trees) - this.Reflection.GetField>(tree, "texture").SetValue(texture); + tree.texture = texture; return true; } -- cgit From 5c1516aaab3a542a73e625e723d4e4425809946c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 18 Nov 2019 21:08:48 -0500 Subject: optimise map tilesheet fixing slightly --- src/SMAPI/Framework/ContentManagers/ModContentManager.cs | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'src') diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index b88bd71e..90b86179 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -240,7 +240,13 @@ namespace StardewModdingAPI.Framework.ContentManagers Color[] data = new Color[texture.Width * texture.Height]; texture.GetData(data); for (int i = 0; i < data.Length; i++) + { + if (data[i].A == 0) + continue; // no need to change fully transparent pixels + data[i] = Color.FromNonPremultiplied(data[i].ToVector4()); + } + texture.SetData(data); return texture; } -- cgit From cc74388025548b028a9429b9d65f99f1413928a3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 23 Nov 2019 22:37:47 -0500 Subject: update compatibility list (#638) --- src/SMAPI.Web/wwwroot/SMAPI.metadata.json | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'src') diff --git a/src/SMAPI.Web/wwwroot/SMAPI.metadata.json b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json index 553b6934..b508b033 100644 --- a/src/SMAPI.Web/wwwroot/SMAPI.metadata.json +++ b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json @@ -147,6 +147,11 @@ /********* ** Broke in SDV 1.4 *********/ + "Fix Dice": { + "ID": "ashley.fixdice", + "~1.1.2 | Status": "AssumeBroken" // crashes game on startup + }, + "Grass Growth": { "ID": "bcmpinc.GrassGrowth", "~1.0 | Status": "AssumeBroken" // runtime Harmony error -- cgit From 730d9783959cc35945841ab721c930fad8ff9ca0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 23 Nov 2019 22:39:57 -0500 Subject: drop mod build package change which sets the x86 platform Visual Studio changes platform inconsistently when set through a NuGet package, which can cause confusing behavior. It's better to set it directly in the project file instead. --- docs/technical/mod-package.md | 11 +++-------- src/SMAPI.ModBuildConfig/build/smapi.targets | 4 ---- src/SMAPI.ModBuildConfig/package.nuspec | 6 ++---- 3 files changed, 5 insertions(+), 16 deletions(-) (limited to 'src') diff --git a/docs/technical/mod-package.md b/docs/technical/mod-package.md index 43682bfb..a33480ad 100644 --- a/docs/technical/mod-package.md +++ b/docs/technical/mod-package.md @@ -130,11 +130,8 @@ To disable game debugging (only needed for some non-mod projects): ``` ### Preconfigure common settings -The package automatically enables PDB files, so error logs show line numbers for simpler debugging. - -For projects using the simplified `.csproj` format, it also enables the GAC (to support XNA -Framework) and sets the build to x86 mode (to avoid 'mismatch between the processor architecture' warnings due to - the game being x86). +The package also automatically enables PDB files (so error logs show line numbers for simpler +debugging), and enables support for the simplified `.csproj` format. ### Add code warnings The package runs code analysis on your mod and raises warnings for some common errors or pitfalls. @@ -294,9 +291,7 @@ which can be uploaded to NuGet or referenced directly. * Updated for SMAPI 3.0 and Stardew Valley 1.4. * Added automatic support for `assets` folders. * Added `$(GameExecutableName)` MSBuild variable. -* Added support for projects using the simplified `.csproj` format: - * platform target is now set to x86 automatically to avoid mismatching platform target warnings; - * added GAC to assembly search paths to fix references to XNA Framework. +* Added support for projects using the simplified `.csproj` format. * Added option to disable game debugging config. * Added `.pdb` files to builds by default (to enable line numbers in error stack traces). * Added optional Harmony reference. diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets index 78d3a3d4..5ca9f032 100644 --- a/src/SMAPI.ModBuildConfig/build/smapi.targets +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -8,10 +8,6 @@ ** Set build options **********************************************--> - - x86 - x86 - pdbonly true diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec index 76818c9f..812e5bcb 100644 --- a/src/SMAPI.ModBuildConfig/package.nuspec +++ b/src/SMAPI.ModBuildConfig/package.nuspec @@ -2,7 +2,7 @@ Pathoschild.Stardew.ModBuildConfig - 3.0.0-beta.6 + 3.0.0 Build package for SMAPI mods Pathoschild Pathoschild @@ -17,9 +17,7 @@ - Updated for SMAPI 3.0 and Stardew Valley 1.4. - Added automatic support for 'assets' folders. - Added $(GameExecutableName) MSBuild variable. - - Added support for projects using the simplified .csproj format: - - platform target is now set to x86 automatically to avoid mismatching platform target warnings; - - added GAC to assembly search paths to fix references to XNA Framework. + - Added support for projects using the simplified .csproj format. - Added option to disable game debugging config. - Added .pdb files to builds by default (to enable line numbers in error stack traces). - Added optional Harmony reference. -- cgit From f0f348bd5f5f4aa27030febe8a057eab9c50db7d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 Nov 2019 12:13:34 -0500 Subject: update packages --- docs/release-notes.md | 2 +- .../SMAPI.ModBuildConfig.Analyzer.Tests.csproj | 2 +- src/SMAPI.Tests/SMAPI.Tests.csproj | 4 ++-- src/SMAPI.Toolkit/SMAPI.Toolkit.csproj | 4 ++-- src/SMAPI.Web/SMAPI.Web.csproj | 16 ++++++++-------- src/SMAPI/SMAPI.csproj | 4 ++-- 6 files changed, 16 insertions(+), 16 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 3771ec19..f11d285c 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -115,7 +115,7 @@ For modders: * Trace logs when loading mods are now more clear. * Clarified update-check errors for mods with multiple update keys. * `SemanticVersion` no longer omits `.0` patch numbers when formatting versions, for better [semver](https://semver.org/) conformity (e.g. `3.0` is now formatted as `3.0.0`). - * Updated dependencies (including Json.NET 11.0.2 → 12.0.2 and Mono.Cecil 0.10.1 → 0.11). + * Updated dependencies (including Json.NET 11.0.2 → 12.0.3 and Mono.Cecil 0.10.1 → 0.11.1). * Fixes: * Fixed custom maps loaded from `.xnb` files not having their tilesheet paths automatically adjusted. * Fixed custom maps loaded from the mod folder with tilesheets in a subfolder not working crossplatform. All tilesheet paths are now normalized for the OS automatically. 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 e7c3e3dc..7e3ce7d4 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj +++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/SMAPI.Tests/SMAPI.Tests.csproj b/src/SMAPI.Tests/SMAPI.Tests.csproj index 84a05aee..639c22a4 100644 --- a/src/SMAPI.Tests/SMAPI.Tests.csproj +++ b/src/SMAPI.Tests/SMAPI.Tests.csproj @@ -16,8 +16,8 @@ - - + + diff --git a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj index a932c41b..3bb7e313 100644 --- a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj +++ b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj @@ -13,8 +13,8 @@ - - + + diff --git a/src/SMAPI.Web/SMAPI.Web.csproj b/src/SMAPI.Web/SMAPI.Web.csproj index c6ee7594..8a7ca741 100644 --- a/src/SMAPI.Web/SMAPI.Web.csproj +++ b/src/SMAPI.Web/SMAPI.Web.csproj @@ -12,19 +12,19 @@ - - - - - + + + + + - - + + - + diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj index 62002a40..4952116f 100644 --- a/src/SMAPI/SMAPI.csproj +++ b/src/SMAPI/SMAPI.csproj @@ -18,8 +18,8 @@ - - + + -- cgit From a5c9cb929e1a9a51a15551475efca84eb932a205 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 Nov 2019 12:36:01 -0500 Subject: migrate package icon to the new NuGet format --- build/prepare-nuget-package.targets | 1 + src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj | 6 ++++++ src/SMAPI.ModBuildConfig/package.nuspec | 7 +++++++ 3 files changed, 14 insertions(+) (limited to 'src') diff --git a/build/prepare-nuget-package.targets b/build/prepare-nuget-package.targets index 4cadae3c..0682d9ff 100644 --- a/build/prepare-nuget-package.targets +++ b/build/prepare-nuget-package.targets @@ -13,6 +13,7 @@ + diff --git a/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj b/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj index df2c7ec8..ccbd9a85 100644 --- a/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj +++ b/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj @@ -33,6 +33,12 @@ + + + PreserveNewest + + + diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec index 812e5bcb..846f438d 100644 --- a/src/SMAPI.ModBuildConfig/package.nuspec +++ b/src/SMAPI.ModBuildConfig/package.nuspec @@ -10,6 +10,7 @@ MIT https://smapi.io/package/readme + images\icon.png https://raw.githubusercontent.com/Pathoschild/SMAPI/develop/src/SMAPI.ModBuildConfig/assets/nuget-icon.png Automates the build configuration for crossplatform Stardew Valley SMAPI mods. For SMAPI 3.0 or later. @@ -24,6 +25,12 @@ - Fixed Newtonsoft.Json.pdb included in release zips when Json.NET is referenced directly. - Fixed <IgnoreModFilePatterns> not working for i18n files. - Dropped support for older versions of SMAPI and Visual Studio. + - Migrated package icon to NuGet's new format. + + + + + -- cgit From 7048f3756312f344dc06a8b98fdce2546d0681f2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 21 Apr 2019 17:54:03 -0400 Subject: add asset propagation for new assets in SDV 1.4 (#638) --- src/SMAPI/Metadata/CoreAssetPropagator.cs | 53 ++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 9bb9a857..4eab12c9 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -8,6 +8,7 @@ using StardewValley; using StardewValley.BellsAndWhistles; using StardewValley.Buildings; using StardewValley.Characters; +using StardewValley.GameData.Movies; using StardewValley.Locations; using StardewValley.Menus; using StardewValley.Objects; @@ -167,10 +168,6 @@ namespace StardewModdingAPI.Metadata /**** ** Animals ****/ - case "animals\\cat": - return this.ReloadPetOrHorseSprites(content, key); - case "animals\\dog": - return this.ReloadPetOrHorseSprites(content, key); case "animals\\horse": return this.ReloadPetOrHorseSprites(content, key); @@ -189,12 +186,14 @@ namespace StardewModdingAPI.Metadata return true; case "characters\\farmer\\farmer_base": // Farmer + case "characters\\farmer\\farmer_base_bald": if (Game1.player == null || !Game1.player.IsMale) return false; Game1.player.FarmerRenderer = new FarmerRenderer(key, Game1.player); return true; case "characters\\farmer\\farmer_girl_base": // Farmer + case "characters\\farmer\\farmer_girl_bald": if (Game1.player == null || Game1.player.IsMale) return false; Game1.player.FarmerRenderer = new FarmerRenderer(key, Game1.player); @@ -208,6 +207,10 @@ namespace StardewModdingAPI.Metadata FarmerRenderer.hatsTexture = content.Load(key); return true; + case "characters\\farmer\\pants": // Game1.LoadContent + FarmerRenderer.pantsTexture = content.Load(key); + return true; + case "characters\\farmer\\shirts": // Game1.LoadContent FarmerRenderer.shirtsTexture = content.Load(key); return true; @@ -223,6 +226,16 @@ namespace StardewModdingAPI.Metadata Game1.bigCraftablesInformation = content.Load>(key); return true; + case "data\\clothinginformation": // Game1.LoadContent + Game1.clothingInformation = content.Load>(key); + return true; + + case "data\\concessiontastes": // MovieTheater.GetConcessionTasteForCharacter + this.Reflection + .GetField>(typeof(MovieTheater), "_concessionTastes") + .SetValue(content.Load>(key)); + return true; + case "data\\cookingrecipes": // CraftingRecipe.InitShared CraftingRecipe.cookingRecipes = content.Load>(key); return true; @@ -234,6 +247,18 @@ namespace StardewModdingAPI.Metadata case "data\\farmanimals": // FarmAnimal constructor return this.ReloadFarmAnimalData(); + case "data\\moviereactions": // MovieTheater.GetMovieReactions + this.Reflection + .GetField>(typeof(MovieTheater), "_genericReactions") + .SetValue(content.Load>(key)); + return true; + + case "data\\movies": // MovieTheater.GetMovieData + this.Reflection + .GetField>(typeof(MovieTheater), "_movieData") + .SetValue(content.Load>(key)); + return true; + case "data\\npcdispositions": // NPC constructor return this.ReloadNpcDispositions(content, key); @@ -241,6 +266,10 @@ namespace StardewModdingAPI.Metadata Game1.NPCGiftTastes = content.Load>(key); return true; + case "data\\objectcontexttags": // Game1.LoadContent + Game1.objectContextTags = content.Load>(key); + return true; + case "data\\objectinformation": // Game1.LoadContent Game1.objectInformation = content.Load>(key); return true; @@ -290,6 +319,14 @@ namespace StardewModdingAPI.Metadata /**** ** Content\LooseSprites ****/ + case "loosesprites\\birds": // Game1.LoadContent + Game1.birdsSpriteSheet = content.Load(key); + return true; + + case "loosesprites\\concessions": // Game1.LoadContent + Game1.concessionsSpriteSheet = content.Load(key); + return true; + case "loosesprites\\controllermaps": // Game1.LoadContent Game1.controllerMaps = content.Load(key); return true; @@ -373,6 +410,10 @@ namespace StardewModdingAPI.Metadata Game1.menuTexture = content.Load(key); return true; + case "maps\\menutilesuncolored": // Game1.LoadContent + Game1.uncoloredMenuTexture = content.Load(key); + return true; + case "maps\\springobjects": // Game1.LoadContent Game1.objectSpriteSheet = content.Load(key); return true; @@ -474,6 +515,10 @@ namespace StardewModdingAPI.Metadata } // dynamic textures + if (this.KeyStartsWith(key, "animals\\cat")) + return this.ReloadPetOrHorseSprites(content, key); + if (this.KeyStartsWith(key, "animals\\dog")) + return this.ReloadPetOrHorseSprites(content, key); if (this.IsInFolder(key, "Animals")) return this.ReloadFarmAnimalSprites(content, key); -- cgit From 2c2644f5a083562822dda48311678daf3b8d5e1f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 19 Jul 2019 21:31:35 -0400 Subject: add roe spawning (#638) --- .../Framework/ItemRepository.cs | 43 +++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs index 4d9091b0..0648aa2b 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs @@ -5,6 +5,7 @@ using System.Linq; using Microsoft.Xna.Framework; using StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData; using StardewValley; +using StardewValley.Menus; using StardewValley.Objects; using StardewValley.Tools; using SObject = StardewValley.Object; @@ -108,7 +109,10 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework // spawn main item SObject item; { - SearchableItem main = this.TryCreate(ItemType.Object, id, () => new SObject(id, 1)); + SearchableItem main = this.TryCreate(ItemType.Object, id, () => id == 812 + ? new ColoredObject(id, 1, Color.White) + : new SObject(id, 1) + ); yield return main; item = main?.Item as SObject; } @@ -189,6 +193,43 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework return honey; }); } + + // roe and aged roe (derived from FishPond.GetFishProduce) + else if (id == 812) + { + foreach (var pair in Game1.objectInformation) + { + // get input + SObject input = new SObject(pair.Key, 1); + if (input.Category != SObject.FishCategory) + continue; + Color color = TailoringMenu.GetDyeColor(input) ?? Color.Orange; + + // yield roe + SObject roe = new ColoredObject(812, 1, color) + { + name = $"{input.Name} Roe", + preserve = { Value = SObject.PreserveType.Roe }, + preservedParentSheetIndex = { Value = input.ParentSheetIndex } + }; + roe.Price += input.Price / 2; + yield return new SearchableItem(ItemType.Object, this.CustomIDOffset * 6 + 1, roe); + + // aged roe + if (pair.Key != 698) // aged sturgeon roe is caviar, which is a separate item + { + ColoredObject agedRoe = new ColoredObject(447, 1, color) + { + name = $"Aged {input.Name} Roe", + Category = -27, + preserve = { Value = SObject.PreserveType.AgedRoe }, + preservedParentSheetIndex = { Value = input.ParentSheetIndex }, + Price = roe.Price * 2 + }; + yield return new SearchableItem(ItemType.Object, this.CustomIDOffset * 6 + 1, agedRoe); + } + } + } } } -- cgit From bb8668db24ef4e46271a0746e0e69e18e5b5264d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 1 Aug 2019 00:45:27 -0400 Subject: update compatibility list (#638) --- src/SMAPI.Web/wwwroot/SMAPI.metadata.json | 37 ++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Web/wwwroot/SMAPI.metadata.json b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json index b508b033..78918bac 100644 --- a/src/SMAPI.Web/wwwroot/SMAPI.metadata.json +++ b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json @@ -126,6 +126,12 @@ "~ | StatusReasonPhrase": "the animal mood bugs were fixed in Stardew Valley 1.2." }, + "Bee House Flower Range Fix": { + "ID": "kirbylink.beehousefix", + "~ | Status": "Obsolete", + "~ | StatusReasonPhrase": "the bee house flower range was fixed in Stardew Valley 1.4." + }, + "Colored Chests": { "ID": "4befde5c-731c-4853-8e4b-c5cdf946805f", "~ | Status": "Obsolete", @@ -152,19 +158,44 @@ "~1.1.2 | Status": "AssumeBroken" // crashes game on startup }, + "Fix Dice": { + "ID": "ashley.fixdice", + "~1.1.2 | Status": "AssumeBroken" // crashes game on startup + }, + "Grass Growth": { "ID": "bcmpinc.GrassGrowth", - "~1.0 | Status": "AssumeBroken" // runtime Harmony error + "~1.0 | Status": "AssumeBroken" + }, + + "Invite Code Mod": { + "ID": "KOREJJamJar.InviteCodeMod", + "~1.0.1 | Status": "AssumeBroken" }, "Loved Labels": { "ID": "Advize.LovedLabels", - "~2.2.1-unofficial.2-pathoschild | Status": "AssumeBroken" // runtime reflection errors + "~2.2.1-unofficial.2-pathoschild | Status": "AssumeBroken" + }, + + "Neat Additions": { + "ID": "ilyaki.neatadditions", + "~1.0.3 | Status": "AssumeBroken" + }, + + "Remote Fridge Storage": { + "ID": "EternalSoap.RemoteFridgeStorage", + "~1.5 | Status": "AssumeBroken" + }, + + "Stack Everything": { + "ID": "cat.stackeverything", + "~2.15 | Status": "AssumeBroken" }, "Yet Another Harvest With Scythe Mod": { "ID": "bcmpinc.HarvestWithScythe", - "~1.1 | Status": "AssumeBroken" // runtime Harmony error + "~1.1 | Status": "AssumeBroken" }, /********* -- cgit From eaf31f992f7bd415f126816de9f1e4f3f3100b73 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 18 Aug 2019 00:43:57 -0400 Subject: update movie data propagation (#638) --- src/SMAPI/Metadata/CoreAssetPropagator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 4eab12c9..1c0a04f0 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -255,8 +255,8 @@ namespace StardewModdingAPI.Metadata case "data\\movies": // MovieTheater.GetMovieData this.Reflection - .GetField>(typeof(MovieTheater), "_movieData") - .SetValue(content.Load>(key)); + .GetField>(typeof(MovieTheater), "_movieData") + .SetValue(content.Load>(key)); return true; case "data\\npcdispositions": // NPC constructor -- cgit