Merge branch 'develop' into stable
This commit is contained in:
commit
c8ad50dad1
|
@ -1,11 +1,15 @@
|
|||
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<!--set general build properties -->
|
||||
<Version>3.13.4</Version>
|
||||
<Version>3.14.0</Version>
|
||||
<Product>SMAPI</Product>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<AssemblySearchPaths>$(AssemblySearchPaths);{GAC}</AssemblySearchPaths>
|
||||
|
||||
<!--enable nullable annotations, except in .NET Standard 2.0 where they aren't supported-->
|
||||
<Nullable Condition="'$(TargetFramework)' != 'netstandard2.0'">enable</Nullable>
|
||||
<NoWarn Condition="'$(TargetFramework)' == 'netstandard2.0'">$(NoWarn);CS8632</NoWarn>
|
||||
|
||||
<!--set platform-->
|
||||
<DefineConstants Condition="$(OS) == 'Windows_NT'">$(DefineConstants);SMAPI_FOR_WINDOWS</DefineConstants>
|
||||
<CopyToGameFolder>true</CopyToGameFolder>
|
||||
|
@ -53,6 +57,7 @@
|
|||
<Copy SourceFiles="$(TargetDir)\SMAPI.metadata.json" DestinationFiles="$(GamePath)\smapi-internal\metadata.json" />
|
||||
<Copy SourceFiles="$(TargetDir)\Newtonsoft.Json.dll" DestinationFolder="$(GamePath)\smapi-internal" />
|
||||
<Copy SourceFiles="$(TargetDir)\TMXTile.dll" DestinationFolder="$(GamePath)\smapi-internal" />
|
||||
<Copy SourceFiles="$(TargetDir)\Pintail.dll" DestinationFolder="$(GamePath)\smapi-internal" />
|
||||
<Copy SourceFiles="@(TranslationFiles)" DestinationFolder="$(GamePath)\smapi-internal\i18n" />
|
||||
|
||||
<!-- Harmony + dependencies -->
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
|
||||
##########
|
||||
## Constants
|
||||
## Fetch values
|
||||
##########
|
||||
# paths
|
||||
gamePath="/home/pathoschild/Stardew Valley"
|
||||
|
@ -21,6 +21,13 @@ folders=("linux" "macOS" "windows")
|
|||
declare -A runtimes=(["linux"]="linux-x64" ["macOS"]="osx-x64" ["windows"]="win-x64")
|
||||
declare -A msBuildPlatformNames=(["linux"]="Unix" ["macOS"]="OSX" ["windows"]="Windows_NT")
|
||||
|
||||
# version number
|
||||
version="$1"
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "SMAPI release version (like '4.0.0'):"
|
||||
read version
|
||||
fi
|
||||
|
||||
|
||||
##########
|
||||
## Move to SMAPI root
|
||||
|
@ -42,6 +49,7 @@ echo ""
|
|||
##########
|
||||
## Compile files
|
||||
##########
|
||||
. ${0%/*}/set-smapi-version.sh "$version"
|
||||
for folder in ${folders[@]}; do
|
||||
runtime=${runtimes[$folder]}
|
||||
msbuildPlatformName=${msBuildPlatformNames[$folder]}
|
||||
|
@ -126,7 +134,7 @@ for folder in ${folders[@]}; do
|
|||
cp -r "$smapiBin/i18n" "$bundlePath/smapi-internal"
|
||||
|
||||
# bundle smapi-internal
|
||||
for name in "0Harmony.dll" "0Harmony.xml" "Mono.Cecil.dll" "Mono.Cecil.Mdb.dll" "Mono.Cecil.Pdb.dll" "MonoMod.Common.dll" "Newtonsoft.Json.dll" "TMXTile.dll" "SMAPI.Toolkit.dll" "SMAPI.Toolkit.pdb" "SMAPI.Toolkit.xml" "SMAPI.Toolkit.CoreInterfaces.dll" "SMAPI.Toolkit.CoreInterfaces.pdb" "SMAPI.Toolkit.CoreInterfaces.xml"; do
|
||||
for name in "0Harmony.dll" "0Harmony.xml" "Mono.Cecil.dll" "Mono.Cecil.Mdb.dll" "Mono.Cecil.Pdb.dll" "MonoMod.Common.dll" "Newtonsoft.Json.dll" "Pintail.dll" "TMXTile.dll" "SMAPI.Toolkit.dll" "SMAPI.Toolkit.pdb" "SMAPI.Toolkit.xml" "SMAPI.Toolkit.CoreInterfaces.dll" "SMAPI.Toolkit.CoreInterfaces.pdb" "SMAPI.Toolkit.CoreInterfaces.xml"; do
|
||||
cp "$smapiBin/$name" "$bundlePath/smapi-internal"
|
||||
done
|
||||
|
||||
|
@ -190,13 +198,6 @@ done
|
|||
##########
|
||||
## Create release zips
|
||||
##########
|
||||
# get version number
|
||||
version="$1"
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "SMAPI release version (like '4.0.0'):"
|
||||
read version
|
||||
fi
|
||||
|
||||
# rename folders
|
||||
mv "$packagePath" "bin/SMAPI $version installer"
|
||||
mv "$packageDevPath" "bin/SMAPI $version installer for developers"
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
#
|
||||
#
|
||||
# This is the PowerShell equivalent of ../unix/prepare-install-package.sh, *except* that it doesn't
|
||||
# set Linux permissions, create the install.dat files, or create the final zip. Due to limitations
|
||||
# in PowerShell, the final changes are handled by the windows/finalize-install-package.sh file in
|
||||
# WSL.
|
||||
# set Linux permissions, create the install.dat files, or create the final zip (unless you specify
|
||||
# --windows-only). Due to limitations in PowerShell, the final changes are handled by the
|
||||
# windows/finalize-install-package.sh file in WSL.
|
||||
#
|
||||
# When making changes, make sure to update ../unix/prepare-install-package.ps1 too.
|
||||
#
|
||||
#
|
||||
|
||||
. "$PSScriptRoot/lib/in-place-regex.ps1"
|
||||
|
||||
. "$PSScriptRoot\lib\in-place-regex.ps1"
|
||||
|
||||
##########
|
||||
## Constants
|
||||
## Fetch values
|
||||
##########
|
||||
# paths
|
||||
$gamePath = "C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley"
|
||||
|
@ -25,6 +25,23 @@ $folders = "linux", "macOS", "windows"
|
|||
$runtimes = @{ linux = "linux-x64"; macOS = "osx-x64"; windows = "win-x64" }
|
||||
$msBuildPlatformNames = @{ linux = "Unix"; macOS = "OSX"; windows = "Windows_NT" }
|
||||
|
||||
# version number
|
||||
$version = $args[0]
|
||||
if (!$version) {
|
||||
$version = Read-Host "SMAPI release version (like '4.0.0')"
|
||||
}
|
||||
|
||||
# Windows-only build
|
||||
$windowsOnly = $false
|
||||
foreach ($arg in $args) {
|
||||
if ($arg -eq "--windows-only") {
|
||||
$windowsOnly = $true
|
||||
$folders = "windows"
|
||||
$runtimes = @{ windows = "win-x64" }
|
||||
$msBuildPlatformNames = @{ windows = "Windows_NT" }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
##########
|
||||
## Move to SMAPI root
|
||||
|
@ -48,7 +65,8 @@ echo ""
|
|||
##########
|
||||
## Compile files
|
||||
##########
|
||||
ForEach ($folder in $folders) {
|
||||
. "$PSScriptRoot/set-smapi-version.ps1" "$version"
|
||||
foreach ($folder in $folders) {
|
||||
$runtime = $runtimes[$folder]
|
||||
$msbuildPlatformName = $msBuildPlatformNames[$folder]
|
||||
|
||||
|
@ -92,6 +110,10 @@ foreach ($folder in $folders) {
|
|||
|
||||
# copy base installer files
|
||||
foreach ($name in @("install on Linux.sh", "install on macOS.command", "install on Windows.bat", "README.txt")) {
|
||||
if ($windowsOnly -and ($name -eq "install on Linux.sh" -or $name -eq "install on macOS.command")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cp "$installAssets/$name" "$packagePath"
|
||||
}
|
||||
|
||||
|
@ -132,7 +154,7 @@ foreach ($folder in $folders) {
|
|||
cp -Recurse "$smapiBin/i18n" "$bundlePath/smapi-internal"
|
||||
|
||||
# bundle smapi-internal
|
||||
foreach ($name in @("0Harmony.dll", "0Harmony.xml", "Mono.Cecil.dll", "Mono.Cecil.Mdb.dll", "Mono.Cecil.Pdb.dll", "MonoMod.Common.dll", "Newtonsoft.Json.dll", "TMXTile.dll", "SMAPI.Toolkit.dll", "SMAPI.Toolkit.pdb", "SMAPI.Toolkit.xml", "SMAPI.Toolkit.CoreInterfaces.dll", "SMAPI.Toolkit.CoreInterfaces.pdb", "SMAPI.Toolkit.CoreInterfaces.xml")) {
|
||||
foreach ($name in @("0Harmony.dll", "0Harmony.xml", "Mono.Cecil.dll", "Mono.Cecil.Mdb.dll", "Mono.Cecil.Pdb.dll", "MonoMod.Common.dll", "Newtonsoft.Json.dll", "Pintail.dll", "TMXTile.dll", "SMAPI.Toolkit.dll", "SMAPI.Toolkit.pdb", "SMAPI.Toolkit.xml", "SMAPI.Toolkit.CoreInterfaces.dll", "SMAPI.Toolkit.CoreInterfaces.pdb", "SMAPI.Toolkit.CoreInterfaces.xml")) {
|
||||
cp "$smapiBin/$name" "$bundlePath/smapi-internal"
|
||||
}
|
||||
|
||||
|
@ -184,34 +206,32 @@ foreach ($folder in $folders) {
|
|||
# disable developer mode in main package
|
||||
In-Place-Regex -Path "$packagePath/internal/$folder/bundle/smapi-internal/config.json" -Search "`"DeveloperMode`": true" -Replace "`"DeveloperMode`": false"
|
||||
|
||||
# DISABLED: will be handled by Linux script
|
||||
# convert bundle folder into final 'install.dat' files
|
||||
#foreach ($path in @("$packagePath/internal/$folder", "$packageDevPath/internal/$folder"))
|
||||
#{
|
||||
# Compress-Archive -Path "$path/bundle/*" -CompressionLevel Optimal -DestinationPath "$path/install.zip"
|
||||
# mv "$path/install.zip" "$path/install.dat"
|
||||
# rm -Recurse -Force "$path/bundle"
|
||||
#}
|
||||
if ($windowsOnly)
|
||||
{
|
||||
foreach ($path in @("$packagePath/internal/$folder", "$packageDevPath/internal/$folder"))
|
||||
{
|
||||
Compress-Archive -Path "$path/bundle/*" -CompressionLevel Optimal -DestinationPath "$path/install.zip"
|
||||
mv "$path/install.zip" "$path/install.dat"
|
||||
rm -Recurse -Force "$path/bundle"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
###########
|
||||
### Create release zips
|
||||
###########
|
||||
# get version number
|
||||
$version = $args[0]
|
||||
if (!$version) {
|
||||
$version = Read-Host "SMAPI release version (like '4.0.0')"
|
||||
}
|
||||
|
||||
# rename folders
|
||||
mv "$packagePath" "bin/SMAPI $version installer"
|
||||
mv "$packageDevPath" "bin/SMAPI $version installer for developers"
|
||||
|
||||
# DISABLED: will be handled by Linux script
|
||||
## package files
|
||||
#Compress-Archive -Path "bin/SMAPI $version installer" -DestinationPath "bin/SMAPI $version installer.zip" -CompressionLevel Optimal
|
||||
#Compress-Archive -Path "bin/SMAPI $version installer for developers" -DestinationPath "bin/SMAPI $version installer for developers.zip" -CompressionLevel Optimal
|
||||
# package files
|
||||
if ($windowsOnly)
|
||||
{
|
||||
Compress-Archive -Path "bin/SMAPI $version installer" -DestinationPath "bin/SMAPI $version installer.zip" -CompressionLevel Optimal
|
||||
Compress-Archive -Path "bin/SMAPI $version installer for developers" -DestinationPath "bin/SMAPI $version installer for developers.zip" -CompressionLevel Optimal
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "Done! See docs/technical/smapi.md to create the release zips."
|
||||
|
|
|
@ -1,6 +1,75 @@
|
|||
← [README](README.md)
|
||||
|
||||
# Release notes
|
||||
## 3.14.0
|
||||
Released 01 May 2022 for Stardew Valley 1.5.6 or later. See [release highlights](https://www.patreon.com/posts/65265507).
|
||||
|
||||
### For players
|
||||
This is a big update, but existing mods should all work fine. If the latest version of a mod breaks in SMAPI 3.14, please report it [on the SMAPI mod page](https://www.nexusmods.com/stardewvalley/mods/2400?tab=posts).
|
||||
|
||||
* Improvements:
|
||||
* SMAPI now ignores dot-prefixed files when searching for mod folders (thanks to Nuztalgia!).
|
||||
* On Linux, SMAPI now fixes many case-sensitive mod path issues automatically.
|
||||
* On Linux/macOS, added `--use-current-shell` [command-line argument](technical/smapi.md#command-line-arguments) to avoid opening a separate terminal window.
|
||||
* Improved performance in some cases.
|
||||
* Improved translations. Thanks to ChulkyBow (updated Ukrainian)!
|
||||
* Dropped update checks for the unofficial 64-bit patcher (obsolete since SMAPI 3.12.6).
|
||||
* Fixes:
|
||||
* Fixed some movie theater textures not translated when loaded through SMAPI (specifically assets with the `_international` suffix).
|
||||
* Fixed the warning text when a mod causes an asset load conflict with itself.
|
||||
* Fixed `--no-terminal` [command-line argument](technical/smapi.md#command-line-arguments) on Linux/macOS still opening a terminal window, even if nothing is logged to it (thanks to Ryhon0!).
|
||||
* Fixed `player_add` console command not handling journal scraps and secret notes correctly.
|
||||
* Fixed `set_farm_type` console command not updating warps.
|
||||
* For the web UI:
|
||||
* Improved log parser UI (thanks to KhloeLeclair!):
|
||||
* Added pagination for big logs.
|
||||
* Added search box to filter the log.
|
||||
* Added option to show/hide content packs in the mod list.
|
||||
* Added jump links in the sidebar.
|
||||
* The filter options now stick to the top of the screen when scrolling.
|
||||
* Rewrote rendering to improve performance.
|
||||
|
||||
### For mod authors
|
||||
This is a big release that includes the new features planned for SMAPI 4.0.0.
|
||||
|
||||
For C# mod authors: your mods should still work fine in SMAPI 3.14.0. However you should review the [migration to SMAPI 4.0](https://stardewvalleywiki.com/Modding:Migrate_to_SMAPI_4.0) guide and update your mods when possible. Deprecated code will be removed when SMAPI 4.0.0 releases later this year (no sooner than August 2022), and break any mods which haven't updated by that time. You can update affected mods now, there's no need to wait for 4.0.0.
|
||||
|
||||
For content pack authors: SMAPI 3.14.0 and 4.0.0 don't affect content packs. They should work fine as long as
|
||||
the C# mod that loads them is updated.
|
||||
|
||||
* Major changes:
|
||||
* Added [content events](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Events#Content), which will replace `IAssetEditor` and `IAssetLoader` in SMAPI 4.0.0.
|
||||
_These include new features not supported by the old API like load conflict resolution, edit priority, and content pack labels. They also support new cases like easily detecting when an asset has changed, and avoid data corruption issues in some edge cases._
|
||||
* Added [nullable reference type annotations](https://stardewvalleywiki.com/Modding:Migrate_to_SMAPI_4.0#Nullable_reference_type_annotations) for all APIs.
|
||||
* Added [`helper.GameContent` and `helper.ModContent`](https://stardewvalleywiki.com/Modding:Migrate_to_SMAPI_4.0#Content_loading_API), which will replace `helper.Content` in SMAPI 4.0.0.
|
||||
* Improved [mod-provided API](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Integrations#Mod-provided_APIs) proxying (thanks to Shockah!).
|
||||
_This adds support for custom interfaces in return values or input arguments, custom enums if their values match, generic methods, and more. This is an internal change, you don't need to do anything different in your mod code._
|
||||
* Mod files loaded through SMAPI APIs (including `helper.Content.Load`) are now case-insensitive, even on Linux.
|
||||
* Enabled deprecation notices for all deprecated APIs. These will only be shown in `TRACE` logs for at least a month after SMAPI 3.14.0 releases.
|
||||
* Other improvements:
|
||||
* Added `IAssetDataForImage.ExtendMap` to resize maps in asset editors.
|
||||
* Added `IContentPack.ModContent` property to manage content pack assets.
|
||||
* Added `Constants.ContentPath` to get the full path to the game's `Content` folder.
|
||||
* Added `IAssetName` fields to the info received by `IAssetEditor`, `IAssetLoader`, and content event methods.
|
||||
_This adds methods for working with asset names, parsed locales, etc._
|
||||
* Added `helper.Content.ParseAssetName` to get an `IAssetName` for an arbitrary asset key.
|
||||
* Added [command-line arguments](technical/smapi.md#command-line-arguments) to toggle developer mode (thanks to Tondorian!).
|
||||
* If an asset is loaded multiple times in the same tick, `IAssetLoader.CanLoad` and `IAssetEditor.CanEdit` are now cached unless invalidated by `helper.Content.InvalidateCache`.
|
||||
* The `ISemanticVersion` comparison methods (`CompareTo`, `IsBetween`, `IsNewerThan`, and `IsOlderThan`) now allow null values. A null version is always considered older than any non-null version per [best practices](https://docs.microsoft.com/en-us/dotnet/api/system.icomparable-1.compareto#remarks).
|
||||
* Deprecation notices now show a shorter stack trace in most cases, so it's clearer where the deprecated code is in the mod.
|
||||
* Fixes:
|
||||
* Fixed the `SDate` constructor being case-sensitive.
|
||||
* Fixed support for using locale codes from custom languages in asset names (e.g. `Data/Achievements.eo-EU`).
|
||||
* Fixed issue where suppressing `[Left|Right]Thumbstick[Down|Left]` keys would suppress the opposite direction instead.
|
||||
* Fixed null handling in various edge cases.
|
||||
* For the web UI:
|
||||
* Updated the JSON validator/schema for Content Patcher 1.25.0.
|
||||
* Added `data-*` attributes to the log parser page for external tools.
|
||||
* Fixed JSON validator showing incorrect error for update keys without a subkey.
|
||||
|
||||
### For SMAPI contributors
|
||||
* You no longer need a Nexus API key to launch the `SMAPI.Web` project locally.
|
||||
|
||||
## 3.13.4
|
||||
Released 16 January 2022 for Stardew Valley 1.5.6 or later.
|
||||
|
||||
|
@ -56,7 +125,7 @@ Released 30 November 2021 for Stardew Valley 1.5.5 or later.
|
|||
* Fixed installer failing on Windows when run from the game folder.
|
||||
|
||||
## 3.13.0
|
||||
Released 30 November 2021 for Stardew Valley 1.5.5 or later.
|
||||
Released 30 November 2021 for Stardew Valley 1.5.5 or later. See [release highlights](https://www.patreon.com/posts/59348226).
|
||||
|
||||
* For players:
|
||||
* Updated for Stardew Valley 1.5.5.
|
||||
|
|
|
@ -412,8 +412,12 @@ The NuGet package is generated automatically in `StardewModdingAPI.ModBuildConfi
|
|||
when you compile it.
|
||||
|
||||
## Release notes
|
||||
## Upcoming release
|
||||
## 4.0.1
|
||||
Released 14 April 2022.
|
||||
|
||||
* Added detection for Xbox app game folders.
|
||||
* Fixed "_conflicts between different versions of Microsoft.Win32.Registry_" warnings in recent SMAPI versions.
|
||||
* Internal refactoring.
|
||||
|
||||
## 4.0.0
|
||||
Released 30 November 2021.
|
||||
|
|
|
@ -33,23 +33,27 @@ argument | purpose
|
|||
`--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 **on Windows only**, but these are intended for internal use
|
||||
SMAPI itself recognises five arguments **on Windows only**, but these are intended for internal use
|
||||
or testing and may change without warning. On Linux/macOS, see _environment variables_ below.
|
||||
|
||||
argument | purpose
|
||||
-------- | -------
|
||||
`--no-terminal` | SMAPI won't write anything to the console window. (Messages will still be written to the log file.)
|
||||
`--developer-mode`<br />`--developer-mode-off` | Enable or disable features intended for mod developers. Currently this only makes `TRACE`-level messages appear in the console.
|
||||
`--no-terminal` | The SMAPI launcher won't try to open a terminal window, and SMAPI won't log anything to the console. (Messages will still be written to the log file.)
|
||||
`--use-current-shell` | The SMAPI launcher won't try to open a terminal window, but SMAPI will still log to the console. (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.
|
||||
|
||||
### Environment variables
|
||||
The above SMAPI arguments don't work on Linux/macOS due to the way the game launcher works. You can
|
||||
set temporary environment variables instead. For example:
|
||||
The above SMAPI arguments may not work on Linux/macOS due to the way the game launcher works. You
|
||||
can set temporary environment variables instead. For example:
|
||||
> SMAPI_MODS_PATH="Mods (multiplayer)" /path/to/StardewValley
|
||||
|
||||
environment variable | purpose
|
||||
-------------------- | -------
|
||||
`SMAPI_NO_TERMINAL` | Equivalent to `--no-terminal` above.
|
||||
`SMAPI_DEVELOPER_MODE` | Equivalent to `--developer-mode` and `--developer-mode-off` above. The value must be `true` or `false`.
|
||||
`SMAPI_MODS_PATH` | Equivalent to `--mods-path` above.
|
||||
`SMAPI_NO_TERMINAL` | Equivalent to `--no-terminal` above.
|
||||
`SMAPI_USE_CURRENT_SHELL` | Equivalent to `--use-current-shell` above.
|
||||
|
||||
### Compile flags
|
||||
SMAPI uses a small number of conditional compilation constants, which you can set by editing the
|
||||
|
@ -79,7 +83,7 @@ folder before compiling.
|
|||
## Prepare a release
|
||||
### On any platform
|
||||
**⚠ Ideally we'd have one set of instructions for all platforms. The instructions in this section
|
||||
will produce a fully functional release for all supported platfrms, _except_ that the application
|
||||
will produce a fully functional release for all supported platforms, _except_ that the application
|
||||
icon for SMAPI on Windows will disappear due to [.NET runtime bug
|
||||
3828](https://github.com/dotnet/runtime/issues/3828). Until that's fixed, see the _[on
|
||||
Windows](#on-windows)_ section below to create a build that retains the icon.**
|
||||
|
@ -116,8 +120,10 @@ Windows](#on-windows)_ section below to create a build that retains the icon.**
|
|||
2. Launch the game through the Steam UI.
|
||||
|
||||
### Prepare the release
|
||||
1. Run `build/unix/set-smapi-version.sh` to set the SMAPI version. Make sure you use a [semantic
|
||||
version](https://semver.org). Recommended format:
|
||||
1. Run `build/unix/prepare-install-package.sh VERSION_HERE` to create the release package in the
|
||||
root `bin` folder.
|
||||
|
||||
Make sure you use a [semantic version](https://semver.org). Recommended format:
|
||||
|
||||
build type | format | example
|
||||
:--------- | :----------------------- | :------
|
||||
|
@ -125,9 +131,6 @@ Windows](#on-windows)_ section below to create a build that retains the icon.**
|
|||
prerelease | `<version>-beta.<date>` | `4.0.0-beta.20251230`
|
||||
release | `<version>` | `4.0.0`
|
||||
|
||||
2. Run `build/unix/prepare-install-package.sh` to create the release package in the root `bin`
|
||||
folder.
|
||||
|
||||
### On Windows
|
||||
#### First-time setup
|
||||
1. Set up Windows Subsystem for Linux (WSL):
|
||||
|
@ -143,8 +146,10 @@ Windows](#on-windows)_ section below to create a build that retains the icon.**
|
|||
```
|
||||
|
||||
### Prepare the release
|
||||
1. Run `build/windows/set-smapi-version.ps1` in PowerShell to set the SMAPI version. Make sure you
|
||||
use a [semantic version](https://semver.org). Recommended format:
|
||||
1. Run `build/windows/prepare-install-package.ps1 VERSION_HERE` in PowerShell to create the release
|
||||
package folders in the root `bin` folder.
|
||||
|
||||
Make sure you use a [semantic version](https://semver.org). Recommended format:
|
||||
|
||||
build type | format | example
|
||||
:--------- | :----------------------- | :------
|
||||
|
@ -152,17 +157,17 @@ Windows](#on-windows)_ section below to create a build that retains the icon.**
|
|||
prerelease | `<version>-beta.<date>` | `4.0.0-beta.20251230`
|
||||
release | `<version>` | `4.0.0`
|
||||
|
||||
2. Run `build/windows/prepare-install-package.ps1` in PowerShell to create the release package
|
||||
folders in the root `bin` folder.
|
||||
|
||||
3. Launch WSL and run this script:
|
||||
2. Launch WSL and run this script:
|
||||
```bash
|
||||
# edit to match the build created in steps 1-2
|
||||
# edit to match the build created in steps 1
|
||||
# In WSL, `/mnt/c/example` accesses `C:\example` on the Windows filesystem.
|
||||
version="4.0.0"
|
||||
binFolder="/mnt/e/source/_Stardew/SMAPI/bin"
|
||||
build/windows/finalize-install-package.sh "$version" "$binFolder"
|
||||
```
|
||||
|
||||
Note: to prepare a test Windows-only build, you can pass `--windows-only` in the first step and
|
||||
skip the second one.
|
||||
|
||||
## Release notes
|
||||
See [release notes](../release-notes.md).
|
||||
|
|
|
@ -12,7 +12,7 @@ namespace StardewModdingAPI.Installer.Framework
|
|||
** Fields
|
||||
*********/
|
||||
/// <summary>The underlying toolkit game scanner.</summary>
|
||||
private readonly GameScanner GameScanner = new GameScanner();
|
||||
private readonly GameScanner GameScanner = new();
|
||||
|
||||
|
||||
/*********
|
||||
|
@ -44,7 +44,7 @@ namespace StardewModdingAPI.Installer.Framework
|
|||
/// <summary>Get the installer's version number.</summary>
|
||||
public ISemanticVersion GetInstallerVersion()
|
||||
{
|
||||
var raw = this.GetType().Assembly.GetName().Version;
|
||||
var raw = this.GetType().Assembly.GetName().Version!;
|
||||
return new SemanticVersion(raw);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
@ -35,6 +36,7 @@ namespace StardewModdingApi.Installer
|
|||
/// <summary>Get the absolute file or folder paths to remove when uninstalling SMAPI.</summary>
|
||||
/// <param name="installDir">The folder for Stardew Valley and SMAPI.</param>
|
||||
/// <param name="modsDir">The folder for SMAPI mods.</param>
|
||||
[SuppressMessage("ReSharper", "StringLiteralTypo", Justification = "These are valid file names.")]
|
||||
private IEnumerable<string> GetUninstallPaths(DirectoryInfo installDir, DirectoryInfo modsDir)
|
||||
{
|
||||
string GetInstallPath(string path) => Path.Combine(installDir.FullName, path);
|
||||
|
@ -126,7 +128,7 @@ namespace StardewModdingApi.Installer
|
|||
/****
|
||||
** Get basic info & set window title
|
||||
****/
|
||||
ModToolkit toolkit = new ModToolkit();
|
||||
ModToolkit toolkit = new();
|
||||
var context = new InstallerContext();
|
||||
Console.Title = $"SMAPI {context.GetInstallerVersion()} installer on {context.Platform} {context.PlatformName}";
|
||||
Console.WriteLine();
|
||||
|
@ -164,7 +166,7 @@ namespace StardewModdingApi.Installer
|
|||
}
|
||||
|
||||
// get game path from CLI
|
||||
string gamePathArg = null;
|
||||
string? gamePathArg = null;
|
||||
{
|
||||
int pathIndex = Array.LastIndexOf(args, "--game-path") + 1;
|
||||
if (pathIndex >= 1 && args.Length >= pathIndex)
|
||||
|
@ -189,8 +191,8 @@ namespace StardewModdingApi.Installer
|
|||
** show theme selector
|
||||
****/
|
||||
// get theme writers
|
||||
var lightBackgroundWriter = new ColorfulConsoleWriter(context.Platform, ColorfulConsoleWriter.GetDefaultColorSchemeConfig(MonitorColorScheme.LightBackground));
|
||||
var darkBackgroundWriter = new ColorfulConsoleWriter(context.Platform, ColorfulConsoleWriter.GetDefaultColorSchemeConfig(MonitorColorScheme.DarkBackground));
|
||||
ColorfulConsoleWriter lightBackgroundWriter = new(context.Platform, ColorfulConsoleWriter.GetDefaultColorSchemeConfig(MonitorColorScheme.LightBackground));
|
||||
ColorfulConsoleWriter darkBackgroundWriter = new(context.Platform, ColorfulConsoleWriter.GetDefaultColorSchemeConfig(MonitorColorScheme.DarkBackground));
|
||||
|
||||
// print question
|
||||
this.PrintPlain("Which text looks more readable?");
|
||||
|
@ -237,7 +239,7 @@ namespace StardewModdingApi.Installer
|
|||
** collect details
|
||||
****/
|
||||
// get game path
|
||||
DirectoryInfo installDir = this.InteractivelyGetInstallPath(toolkit, context, gamePathArg);
|
||||
DirectoryInfo? installDir = this.InteractivelyGetInstallPath(toolkit, context, gamePathArg);
|
||||
if (installDir == null)
|
||||
{
|
||||
this.PrintError("Failed finding your game path.");
|
||||
|
@ -246,7 +248,7 @@ namespace StardewModdingApi.Installer
|
|||
}
|
||||
|
||||
// get folders
|
||||
DirectoryInfo bundleDir = new DirectoryInfo(this.BundlePath);
|
||||
DirectoryInfo bundleDir = new(this.BundlePath);
|
||||
paths = new InstallerPaths(bundleDir, installDir);
|
||||
}
|
||||
|
||||
|
@ -354,8 +356,8 @@ namespace StardewModdingApi.Installer
|
|||
// move global save data folder (changed in 3.2)
|
||||
{
|
||||
string dataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "StardewValley");
|
||||
DirectoryInfo oldDir = new DirectoryInfo(Path.Combine(dataPath, "Saves", ".smapi"));
|
||||
DirectoryInfo newDir = new DirectoryInfo(Path.Combine(dataPath, ".smapi"));
|
||||
DirectoryInfo oldDir = new(Path.Combine(dataPath, "Saves", ".smapi"));
|
||||
DirectoryInfo newDir = new(Path.Combine(dataPath, ".smapi"));
|
||||
|
||||
if (oldDir.Exists)
|
||||
{
|
||||
|
@ -428,7 +430,7 @@ namespace StardewModdingApi.Installer
|
|||
}
|
||||
|
||||
// add or replace bundled mods
|
||||
DirectoryInfo bundledModsDir = new DirectoryInfo(Path.Combine(paths.BundlePath, "Mods"));
|
||||
DirectoryInfo bundledModsDir = new(Path.Combine(paths.BundlePath, "Mods"));
|
||||
if (bundledModsDir.Exists && bundledModsDir.EnumerateDirectories().Any())
|
||||
{
|
||||
this.PrintDebug("Adding bundled mods...");
|
||||
|
@ -449,8 +451,8 @@ namespace StardewModdingApi.Installer
|
|||
}
|
||||
|
||||
// find target folder
|
||||
ModFolder targetMod = targetMods.FirstOrDefault(p => p.Manifest?.UniqueID?.Equals(sourceMod.Manifest.UniqueID, StringComparison.OrdinalIgnoreCase) == true);
|
||||
DirectoryInfo defaultTargetFolder = new DirectoryInfo(Path.Combine(paths.ModsPath, sourceMod.Directory.Name));
|
||||
ModFolder? targetMod = targetMods.FirstOrDefault(p => p.Manifest?.UniqueID?.Equals(sourceMod.Manifest.UniqueID, StringComparison.OrdinalIgnoreCase) == true);
|
||||
DirectoryInfo defaultTargetFolder = new(Path.Combine(paths.ModsPath, sourceMod.Directory.Name));
|
||||
DirectoryInfo targetFolder = targetMod?.Directory ?? defaultTargetFolder;
|
||||
this.PrintDebug(targetFolder.FullName == defaultTargetFolder.FullName
|
||||
? $" adding {sourceMod.Manifest.Name}..."
|
||||
|
@ -532,27 +534,45 @@ namespace StardewModdingApi.Installer
|
|||
|
||||
/// <summary>Print a message without formatting.</summary>
|
||||
/// <param name="text">The text to print.</param>
|
||||
private void PrintPlain(string text) => Console.WriteLine(text);
|
||||
private void PrintPlain(string text)
|
||||
{
|
||||
Console.WriteLine(text);
|
||||
}
|
||||
|
||||
/// <summary>Print a debug message.</summary>
|
||||
/// <param name="text">The text to print.</param>
|
||||
private void PrintDebug(string text) => this.ConsoleWriter.WriteLine(text, ConsoleLogLevel.Debug);
|
||||
private void PrintDebug(string text)
|
||||
{
|
||||
this.ConsoleWriter.WriteLine(text, ConsoleLogLevel.Debug);
|
||||
}
|
||||
|
||||
/// <summary>Print a debug message.</summary>
|
||||
/// <param name="text">The text to print.</param>
|
||||
private void PrintInfo(string text) => this.ConsoleWriter.WriteLine(text, ConsoleLogLevel.Info);
|
||||
private void PrintInfo(string text)
|
||||
{
|
||||
this.ConsoleWriter.WriteLine(text, ConsoleLogLevel.Info);
|
||||
}
|
||||
|
||||
/// <summary>Print a warning message.</summary>
|
||||
/// <param name="text">The text to print.</param>
|
||||
private void PrintWarning(string text) => this.ConsoleWriter.WriteLine(text, ConsoleLogLevel.Warn);
|
||||
private void PrintWarning(string text)
|
||||
{
|
||||
this.ConsoleWriter.WriteLine(text, ConsoleLogLevel.Warn);
|
||||
}
|
||||
|
||||
/// <summary>Print a warning message.</summary>
|
||||
/// <param name="text">The text to print.</param>
|
||||
private void PrintError(string text) => this.ConsoleWriter.WriteLine(text, ConsoleLogLevel.Error);
|
||||
private void PrintError(string text)
|
||||
{
|
||||
this.ConsoleWriter.WriteLine(text, ConsoleLogLevel.Error);
|
||||
}
|
||||
|
||||
/// <summary>Print a success message.</summary>
|
||||
/// <param name="text">The text to print.</param>
|
||||
private void PrintSuccess(string text) => this.ConsoleWriter.WriteLine(text, ConsoleLogLevel.Success);
|
||||
private void PrintSuccess(string text)
|
||||
{
|
||||
this.ConsoleWriter.WriteLine(text, ConsoleLogLevel.Success);
|
||||
}
|
||||
|
||||
/// <summary>Interactively delete a file or folder path, and block until deletion completes.</summary>
|
||||
/// <param name="path">The file or folder path.</param>
|
||||
|
@ -562,7 +582,7 @@ namespace StardewModdingApi.Installer
|
|||
{
|
||||
try
|
||||
{
|
||||
FileUtilities.ForceDelete(Directory.Exists(path) ? new DirectoryInfo(path) : (FileSystemInfo)new FileInfo(path));
|
||||
FileUtilities.ForceDelete(Directory.Exists(path) ? new DirectoryInfo(path) : new FileInfo(path));
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
@ -578,7 +598,7 @@ namespace StardewModdingApi.Installer
|
|||
/// <param name="source">The file or folder to copy.</param>
|
||||
/// <param name="targetFolder">The folder to copy into.</param>
|
||||
/// <param name="filter">A filter which matches directories and files to copy, or <c>null</c> to match all.</param>
|
||||
private void RecursiveCopy(FileSystemInfo source, DirectoryInfo targetFolder, Func<FileSystemInfo, bool> filter = null)
|
||||
private void RecursiveCopy(FileSystemInfo source, DirectoryInfo targetFolder, Func<FileSystemInfo, bool>? filter = null)
|
||||
{
|
||||
if (filter != null && !filter(source))
|
||||
return;
|
||||
|
@ -593,8 +613,8 @@ namespace StardewModdingApi.Installer
|
|||
break;
|
||||
|
||||
case DirectoryInfo sourceDir:
|
||||
DirectoryInfo targetSubfolder = new DirectoryInfo(Path.Combine(targetFolder.FullName, sourceDir.Name));
|
||||
foreach (var entry in sourceDir.EnumerateFileSystemInfos())
|
||||
DirectoryInfo targetSubfolder = new(Path.Combine(targetFolder.FullName, sourceDir.Name));
|
||||
foreach (FileSystemInfo entry in sourceDir.EnumerateFileSystemInfos())
|
||||
this.RecursiveCopy(entry, targetSubfolder, filter);
|
||||
break;
|
||||
|
||||
|
@ -608,7 +628,7 @@ namespace StardewModdingApi.Installer
|
|||
/// <param name="message">The message to print.</param>
|
||||
/// <param name="options">The allowed options (not case sensitive).</param>
|
||||
/// <param name="indent">The indentation to prefix to output.</param>
|
||||
private string InteractivelyChoose(string message, string[] options, string indent = "", Action<string> print = null)
|
||||
private string InteractivelyChoose(string message, string[] options, string indent = "", Action<string>? print = null)
|
||||
{
|
||||
print ??= this.PrintInfo;
|
||||
|
||||
|
@ -616,8 +636,8 @@ namespace StardewModdingApi.Installer
|
|||
{
|
||||
print(indent + message);
|
||||
Console.Write(indent);
|
||||
string input = Console.ReadLine()?.Trim().ToLowerInvariant();
|
||||
if (!options.Contains(input))
|
||||
string? input = Console.ReadLine()?.Trim().ToLowerInvariant();
|
||||
if (input == null || !options.Contains(input))
|
||||
{
|
||||
print($"{indent}That's not a valid option.");
|
||||
continue;
|
||||
|
@ -630,7 +650,7 @@ namespace StardewModdingApi.Installer
|
|||
/// <param name="toolkit">The mod toolkit.</param>
|
||||
/// <param name="context">The installer context.</param>
|
||||
/// <param name="specifiedPath">The path specified as a command-line argument (if any), which should override automatic path detection.</param>
|
||||
private DirectoryInfo InteractivelyGetInstallPath(ModToolkit toolkit, InstallerContext context, string specifiedPath)
|
||||
private DirectoryInfo? InteractivelyGetInstallPath(ModToolkit toolkit, InstallerContext context, string? specifiedPath)
|
||||
{
|
||||
// use specified path
|
||||
if (specifiedPath != null)
|
||||
|
@ -697,7 +717,7 @@ namespace StardewModdingApi.Installer
|
|||
// get path from user
|
||||
Console.WriteLine();
|
||||
this.PrintInfo($"Type the file path to the game directory (the one containing '{Constants.GameDllName}'), then press enter.");
|
||||
string path = Console.ReadLine()?.Trim();
|
||||
string? path = Console.ReadLine()?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
this.PrintWarning("You must specify a directory path to continue.");
|
||||
|
@ -710,14 +730,14 @@ namespace StardewModdingApi.Installer
|
|||
: path.Replace("\\ ", " "); // in Linux/macOS, spaces in paths may be escaped if copied from the command line
|
||||
if (path.StartsWith("~/"))
|
||||
{
|
||||
string home = Environment.GetEnvironmentVariable("HOME") ?? Environment.GetEnvironmentVariable("USERPROFILE");
|
||||
string home = Environment.GetEnvironmentVariable("HOME") ?? Environment.GetEnvironmentVariable("USERPROFILE")!;
|
||||
path = Path.Combine(home, path.Substring(2));
|
||||
}
|
||||
|
||||
// get directory
|
||||
if (File.Exists(path))
|
||||
path = Path.GetDirectoryName(path);
|
||||
DirectoryInfo directory = new DirectoryInfo(path);
|
||||
path = Path.GetDirectoryName(path)!;
|
||||
DirectoryInfo directory = new(path);
|
||||
|
||||
// validate path
|
||||
if (!directory.Exists)
|
||||
|
@ -763,7 +783,7 @@ namespace StardewModdingApi.Installer
|
|||
|
||||
// game folder which contains the installer, if any
|
||||
{
|
||||
DirectoryInfo curPath = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory;
|
||||
DirectoryInfo? curPath = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory;
|
||||
while (curPath?.Parent != null) // must be in a folder (not at the root)
|
||||
{
|
||||
if (context.LooksLikeGameFolder(curPath))
|
||||
|
@ -785,7 +805,7 @@ namespace StardewModdingApi.Installer
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>Interactively move mods out of the appdata directory.</summary>
|
||||
/// <summary>Interactively move mods out of the app data directory.</summary>
|
||||
/// <param name="properModsDir">The directory which should contain all mods.</param>
|
||||
/// <param name="packagedModsDir">The installer directory containing packaged mods.</param>
|
||||
private void InteractivelyRemoveAppDataMods(DirectoryInfo properModsDir, DirectoryInfo packagedModsDir)
|
||||
|
@ -795,7 +815,7 @@ namespace StardewModdingApi.Installer
|
|||
|
||||
// get path
|
||||
string appDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "StardewValley");
|
||||
DirectoryInfo modDir = new DirectoryInfo(Path.Combine(appDataPath, "Mods"));
|
||||
DirectoryInfo modDir = new(Path.Combine(appDataPath, "Mods"));
|
||||
|
||||
// check if migration needed
|
||||
if (!modDir.Exists)
|
||||
|
@ -808,7 +828,7 @@ namespace StardewModdingApi.Installer
|
|||
{
|
||||
// get type
|
||||
bool isDir = entry is DirectoryInfo;
|
||||
if (!isDir && !(entry is FileInfo))
|
||||
if (!isDir && entry is not FileInfo)
|
||||
continue; // should never happen
|
||||
|
||||
// delete packaged mods (newer version bundled into SMAPI)
|
||||
|
@ -845,7 +865,7 @@ namespace StardewModdingApi.Installer
|
|||
/// <summary>Move a filesystem entry to a new parent directory.</summary>
|
||||
/// <param name="entry">The filesystem entry to move.</param>
|
||||
/// <param name="newPath">The destination path.</param>
|
||||
/// <remarks>We can't use <see cref="FileInfo.MoveTo"/> or <see cref="DirectoryInfo.MoveTo"/>, because those don't work across partitions.</remarks>
|
||||
/// <remarks>We can't use <see cref="FileInfo.MoveTo(string)"/> or <see cref="DirectoryInfo.MoveTo"/>, because those don't work across partitions.</remarks>
|
||||
private void Move(FileSystemInfo entry, string newPath)
|
||||
{
|
||||
// file
|
||||
|
@ -872,15 +892,12 @@ namespace StardewModdingApi.Installer
|
|||
/// <param name="entry">The file or folder info.</param>
|
||||
private bool ShouldCopy(FileSystemInfo entry)
|
||||
{
|
||||
switch (entry.Name)
|
||||
return entry.Name switch
|
||||
{
|
||||
case "mcs":
|
||||
return false; // ignore macOS symlink
|
||||
case "Mods":
|
||||
return false; // Mods folder handled separately
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
"mcs" => false, // ignore macOS symlink
|
||||
"Mods" => false, // Mods folder handled separately
|
||||
_ => true
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ namespace StardewModdingApi.Installer
|
|||
*********/
|
||||
/// <summary>The absolute path of the installer folder.</summary>
|
||||
[SuppressMessage("ReSharper", "AssignNullToNotNullAttribute", Justification = "The assembly location is never null in this context.")]
|
||||
private static readonly string InstallerPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
|
||||
private static readonly string InstallerPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
|
||||
|
||||
/// <summary>The absolute path of the folder containing the unzipped installer files.</summary>
|
||||
private static readonly string ExtractedBundlePath = Path.Combine(Path.GetTempPath(), $"SMAPI-installer-{Guid.NewGuid():N}");
|
||||
|
@ -31,7 +31,7 @@ namespace StardewModdingApi.Installer
|
|||
public static void Main(string[] args)
|
||||
{
|
||||
// find install bundle
|
||||
FileInfo zipFile = new FileInfo(Path.Combine(Program.InstallerPath, "install.dat"));
|
||||
FileInfo zipFile = new(Path.Combine(Program.InstallerPath, "install.dat"));
|
||||
if (!zipFile.Exists)
|
||||
{
|
||||
Console.WriteLine($"Oops! Some of the installer files are missing; try re-downloading the installer. (Missing file: {zipFile.FullName})");
|
||||
|
@ -40,7 +40,7 @@ namespace StardewModdingApi.Installer
|
|||
}
|
||||
|
||||
// unzip bundle into temp folder
|
||||
DirectoryInfo bundleDir = new DirectoryInfo(Program.ExtractedBundlePath);
|
||||
DirectoryInfo bundleDir = new(Program.ExtractedBundlePath);
|
||||
Console.WriteLine("Extracting install files...");
|
||||
ZipFile.ExtractToDirectory(zipFile.FullName, bundleDir.FullName);
|
||||
|
||||
|
@ -66,14 +66,14 @@ namespace StardewModdingApi.Installer
|
|||
/// <summary>Method called when assembly resolution fails, which may return a manually resolved assembly.</summary>
|
||||
/// <param name="sender">The event sender.</param>
|
||||
/// <param name="e">The event arguments.</param>
|
||||
private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs e)
|
||||
private static Assembly? CurrentDomain_AssemblyResolve(object? sender, ResolveEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
AssemblyName name = new AssemblyName(e.Name);
|
||||
AssemblyName name = new(e.Name);
|
||||
foreach (FileInfo dll in new DirectoryInfo(Program.InternalFilesPath).EnumerateFiles("*.dll"))
|
||||
{
|
||||
if (name.Name.Equals(AssemblyName.GetAssemblyName(dll.FullName).Name, StringComparison.OrdinalIgnoreCase))
|
||||
if (name.Name != null && name.Name.Equals(AssemblyName.GetAssemblyName(dll.FullName).Name, StringComparison.OrdinalIgnoreCase))
|
||||
return Assembly.LoadFrom(dll.FullName);
|
||||
}
|
||||
return null;
|
||||
|
|
|
@ -6,10 +6,41 @@
|
|||
# move to script's directory
|
||||
cd "$(dirname "$0")" || exit $?
|
||||
|
||||
# change to true to skip opening a terminal
|
||||
# Whether to avoid opening a separate terminal window, and avoid logging anything to the console.
|
||||
# This isn't recommended since you won't see errors, warnings, and update alerts.
|
||||
SKIP_TERMINAL=false
|
||||
|
||||
# Whether to avoid opening a separate terminal, but still send the usual log output to the console.
|
||||
USE_CURRENT_SHELL=false
|
||||
|
||||
|
||||
##########
|
||||
## Read environment variables
|
||||
##########
|
||||
if [ "$SMAPI_NO_TERMINAL" == "true" ]; then
|
||||
SKIP_TERMINAL=true
|
||||
fi
|
||||
if [ "$SMAPI_USE_CURRENT_SHELL" == "true" ]; then
|
||||
USE_CURRENT_SHELL=true
|
||||
fi
|
||||
|
||||
|
||||
##########
|
||||
## Read command-line arguments
|
||||
##########
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--skip-terminal ) SKIP_TERMINAL=true; shift ;;
|
||||
--use-current-shell ) USE_CURRENT_SHELL=true; shift ;;
|
||||
-- ) shift; break ;;
|
||||
* ) shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$SKIP_TERMINAL" == "true" ]; then
|
||||
USE_CURRENT_SHELL=true
|
||||
fi
|
||||
|
||||
|
||||
##########
|
||||
## Open terminal if needed
|
||||
|
@ -18,21 +49,13 @@ SKIP_TERMINAL=false
|
|||
# Besides letting the player see errors/warnings/alerts in the console, this is also needed because
|
||||
# Steam messes with the PATH.
|
||||
if [ "$(uname)" == "Darwin" ]; then
|
||||
if [ ! -t 1 ]; then # https://stackoverflow.com/q/911168/262123
|
||||
# sanity check to make sure we don't have an infinite loop of opening windows
|
||||
for argument in "$@"; do
|
||||
if [ "$argument" == "--no-reopen-terminal" ]; then
|
||||
SKIP_TERMINAL=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ! -t 1 ]; then # not open in Terminal (https://stackoverflow.com/q/911168/262123)
|
||||
# reopen in Terminal if needed
|
||||
# https://stackoverflow.com/a/29511052/262123
|
||||
if [ "$SKIP_TERMINAL" == "false" ]; then
|
||||
if [ "$USE_CURRENT_SHELL" == "false" ]; then
|
||||
echo "Reopening in the Terminal app..."
|
||||
echo '#!/bin/sh' > /tmp/open-smapi-terminal.sh
|
||||
echo "\"$0\" $@ --no-reopen-terminal" >> /tmp/open-smapi-terminal.sh
|
||||
echo "\"$0\" $@ --use-current-shell" >> /tmp/open-smapi-terminal.sh
|
||||
chmod +x /tmp/open-smapi-terminal.sh
|
||||
cat /tmp/open-smapi-terminal.sh
|
||||
open -W -a Terminal /tmp/open-smapi-terminal.sh
|
||||
|
@ -68,7 +91,7 @@ else
|
|||
export LAUNCH_FILE
|
||||
|
||||
# run in terminal
|
||||
if [ "$SKIP_TERMINAL" == "false" ]; then
|
||||
if [ "$USE_CURRENT_SHELL" == "false" ]; then
|
||||
# select terminal (prefer xterm for best compatibility, then known supported terminals)
|
||||
for terminal in xterm gnome-terminal kitty terminator xfce4-terminal konsole terminal termite alacritty mate-terminal x-terminal-emulator; do
|
||||
if command -v "$terminal" 2>/dev/null; then
|
||||
|
@ -131,7 +154,9 @@ else
|
|||
fi
|
||||
|
||||
# explicitly run without terminal
|
||||
else
|
||||
elif [ "$SKIP_TERMINAL" == "true" ]; then
|
||||
exec $LAUNCH_FILE --no-terminal "$@"
|
||||
else
|
||||
exec $LAUNCH_FILE "$@"
|
||||
fi
|
||||
fi
|
||||
|
|
|
@ -30,7 +30,7 @@ namespace StardewModdingAPI.Internal.Patching
|
|||
/// <param name="name">The method name.</param>
|
||||
/// <param name="parameters">The method parameter types, or <c>null</c> if it's not overloaded.</param>
|
||||
/// <param name="generics">The method generic types, or <c>null</c> if it's not generic.</param>
|
||||
protected MethodInfo RequireMethod<TTarget>(string name, Type[] parameters = null, Type[] generics = null)
|
||||
protected MethodInfo RequireMethod<TTarget>(string name, Type[]? parameters = null, Type[]? generics = null)
|
||||
{
|
||||
return PatchHelper.RequireMethod<TTarget>(name, parameters, generics);
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ namespace StardewModdingAPI.Internal.Patching
|
|||
/// <param name="priority">The patch priority to apply, usually specified using Harmony's <see cref="Priority"/> enum, or <c>null</c> to keep the default value.</param>
|
||||
protected HarmonyMethod GetHarmonyMethod(string name, int? priority = null)
|
||||
{
|
||||
var method = new HarmonyMethod(
|
||||
HarmonyMethod method = new(
|
||||
AccessTools.Method(this.GetType(), name)
|
||||
?? throw new InvalidOperationException($"Can't find patcher method {PatchHelper.GetMethodString(this.GetType(), name)}.")
|
||||
);
|
||||
|
|
|
@ -15,7 +15,7 @@ namespace StardewModdingAPI.Internal.Patching
|
|||
/// <param name="patchers">The patchers to apply.</param>
|
||||
public static Harmony Apply(string id, IMonitor monitor, params IPatcher[] patchers)
|
||||
{
|
||||
Harmony harmony = new Harmony(id);
|
||||
Harmony harmony = new(id);
|
||||
|
||||
foreach (IPatcher patcher in patchers)
|
||||
{
|
||||
|
|
|
@ -16,7 +16,7 @@ namespace StardewModdingAPI.Internal.Patching
|
|||
/// <typeparam name="TTarget">The type containing the method.</typeparam>
|
||||
/// <param name="parameters">The method parameter types, or <c>null</c> if it's not overloaded.</param>
|
||||
/// <exception cref="InvalidOperationException">The type has no matching constructor.</exception>
|
||||
public static ConstructorInfo RequireConstructor<TTarget>(Type[] parameters = null)
|
||||
public static ConstructorInfo RequireConstructor<TTarget>(Type[]? parameters = null)
|
||||
{
|
||||
return
|
||||
AccessTools.Constructor(typeof(TTarget), parameters)
|
||||
|
@ -29,7 +29,7 @@ namespace StardewModdingAPI.Internal.Patching
|
|||
/// <param name="parameters">The method parameter types, or <c>null</c> if it's not overloaded.</param>
|
||||
/// <param name="generics">The method generic types, or <c>null</c> if it's not generic.</param>
|
||||
/// <exception cref="InvalidOperationException">The type has no matching method.</exception>
|
||||
public static MethodInfo RequireMethod<TTarget>(string name, Type[] parameters = null, Type[] generics = null)
|
||||
public static MethodInfo RequireMethod<TTarget>(string name, Type[]? parameters = null, Type[]? generics = null)
|
||||
{
|
||||
return
|
||||
AccessTools.Method(typeof(TTarget), name, parameters, generics)
|
||||
|
@ -41,9 +41,9 @@ namespace StardewModdingAPI.Internal.Patching
|
|||
/// <param name="name">The method name, or <c>null</c> for a constructor.</param>
|
||||
/// <param name="parameters">The method parameter types, or <c>null</c> if it's not overloaded.</param>
|
||||
/// <param name="generics">The method generic types, or <c>null</c> if it's not generic.</param>
|
||||
public static string GetMethodString(Type type, string name, Type[] parameters = null, Type[] generics = null)
|
||||
public static string GetMethodString(Type type, string? name, Type[]? parameters = null, Type[]? generics = null)
|
||||
{
|
||||
StringBuilder str = new StringBuilder();
|
||||
StringBuilder str = new();
|
||||
|
||||
// type
|
||||
str.Append(type.FullName);
|
||||
|
|
|
@ -6,10 +6,26 @@ namespace StardewModdingAPI.Internal.ConsoleWriting
|
|||
/// <summary>The console color scheme options.</summary>
|
||||
internal class ColorSchemeConfig
|
||||
{
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The default color scheme ID to use, or <see cref="MonitorColorScheme.AutoDetect"/> to select one automatically.</summary>
|
||||
public MonitorColorScheme UseScheme { get; set; }
|
||||
public MonitorColorScheme UseScheme { get; }
|
||||
|
||||
/// <summary>The available console color schemes.</summary>
|
||||
public IDictionary<MonitorColorScheme, IDictionary<ConsoleLogLevel, ConsoleColor>> Schemes { get; set; }
|
||||
public IDictionary<MonitorColorScheme, IDictionary<ConsoleLogLevel, ConsoleColor>> Schemes { get; }
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="useScheme">The default color scheme ID to use, or <see cref="MonitorColorScheme.AutoDetect"/> to select one automatically.</param>
|
||||
/// <param name="schemes">The available console color schemes.</param>
|
||||
public ColorSchemeConfig(MonitorColorScheme useScheme, IDictionary<MonitorColorScheme, IDictionary<ConsoleLogLevel, ConsoleColor>> schemes)
|
||||
{
|
||||
this.UseScheme = useScheme;
|
||||
this.Schemes = schemes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using StardewModdingAPI.Toolkit.Utilities;
|
||||
|
||||
namespace StardewModdingAPI.Internal.ConsoleWriting
|
||||
|
@ -11,10 +12,11 @@ namespace StardewModdingAPI.Internal.ConsoleWriting
|
|||
** Fields
|
||||
*********/
|
||||
/// <summary>The console text color for each log level.</summary>
|
||||
private readonly IDictionary<ConsoleLogLevel, ConsoleColor> Colors;
|
||||
private readonly IDictionary<ConsoleLogLevel, ConsoleColor>? Colors;
|
||||
|
||||
/// <summary>Whether the current console supports color formatting.</summary>
|
||||
private readonly bool SupportsColor;
|
||||
[MemberNotNullWhen(true, nameof(ColorfulConsoleWriter.Colors))]
|
||||
private bool SupportsColor { get; }
|
||||
|
||||
|
||||
/*********
|
||||
|
@ -72,10 +74,9 @@ namespace StardewModdingAPI.Internal.ConsoleWriting
|
|||
/// <remarks>The colors here should be kept in sync with the SMAPI config file.</remarks>
|
||||
public static ColorSchemeConfig GetDefaultColorSchemeConfig(MonitorColorScheme useScheme)
|
||||
{
|
||||
return new ColorSchemeConfig
|
||||
{
|
||||
UseScheme = useScheme,
|
||||
Schemes = new Dictionary<MonitorColorScheme, IDictionary<ConsoleLogLevel, ConsoleColor>>
|
||||
return new ColorSchemeConfig(
|
||||
useScheme: useScheme,
|
||||
schemes: new Dictionary<MonitorColorScheme, IDictionary<ConsoleLogLevel, ConsoleColor>>
|
||||
{
|
||||
[MonitorColorScheme.DarkBackground] = new Dictionary<ConsoleLogLevel, ConsoleColor>
|
||||
{
|
||||
|
@ -98,7 +99,7 @@ namespace StardewModdingAPI.Internal.ConsoleWriting
|
|||
[ConsoleLogLevel.Success] = ConsoleColor.DarkGreen
|
||||
}
|
||||
}
|
||||
};
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
@ -134,7 +135,7 @@ namespace StardewModdingAPI.Internal.ConsoleWriting
|
|||
}
|
||||
|
||||
// get colors for scheme
|
||||
return colorConfig.Schemes.TryGetValue(schemeID, out IDictionary<ConsoleLogLevel, ConsoleColor> scheme)
|
||||
return colorConfig.Schemes.TryGetValue(schemeID, out IDictionary<ConsoleLogLevel, ConsoleColor>? scheme)
|
||||
? scheme
|
||||
: throw new NotSupportedException($"Unknown color scheme '{schemeID}'.");
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ namespace StardewModdingAPI.Internal
|
|||
*********/
|
||||
/// <summary>Get a string representation of an exception suitable for writing to the error log.</summary>
|
||||
/// <param name="exception">The error to summarize.</param>
|
||||
public static string GetLogSummary(this Exception exception)
|
||||
public static string GetLogSummary(this Exception? exception)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
@ -25,7 +25,7 @@ namespace StardewModdingAPI.Internal
|
|||
|
||||
case ReflectionTypeLoadException ex:
|
||||
string summary = ex.ToString();
|
||||
foreach (Exception childEx in ex.LoaderExceptions ?? new Exception[0])
|
||||
foreach (Exception? childEx in ex.LoaderExceptions)
|
||||
summary += $"\n\n{childEx?.GetLogSummary()}";
|
||||
message = summary;
|
||||
break;
|
||||
|
@ -43,15 +43,6 @@ namespace StardewModdingAPI.Internal
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>Get the lowest exception in an exception stack.</summary>
|
||||
/// <param name="exception">The exception from which to search.</param>
|
||||
public static Exception GetInnermostException(this Exception exception)
|
||||
{
|
||||
while (exception.InnerException != null)
|
||||
exception = exception.InnerException;
|
||||
return exception;
|
||||
}
|
||||
|
||||
/// <summary>Simplify common patterns in exception log messages that don't convey useful info.</summary>
|
||||
/// <param name="message">The log message to simplify.</param>
|
||||
public static string SimplifyExtensionMessage(string message)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// <generated />
|
||||
using Microsoft.CodeAnalysis;
|
||||
// ReSharper disable All -- generated code
|
||||
using System;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
|
||||
{
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
// <generated />
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
// ReSharper disable All -- generated code
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
|
||||
namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
|
||||
{
|
||||
|
@ -51,17 +53,17 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
|
|||
protected static Diagnostic[] GetSortedDiagnosticsFromDocuments(DiagnosticAnalyzer analyzer, Document[] documents)
|
||||
{
|
||||
var projects = new HashSet<Project>();
|
||||
foreach (var document in documents)
|
||||
foreach (Document document in documents)
|
||||
{
|
||||
projects.Add(document.Project);
|
||||
}
|
||||
|
||||
var diagnostics = new List<Diagnostic>();
|
||||
foreach (var project in projects)
|
||||
foreach (Project project in projects)
|
||||
{
|
||||
var compilationWithAnalyzers = project.GetCompilationAsync().Result.WithAnalyzers(ImmutableArray.Create(analyzer));
|
||||
CompilationWithAnalyzers compilationWithAnalyzers = project.GetCompilationAsync().Result!.WithAnalyzers(ImmutableArray.Create(analyzer));
|
||||
var diags = compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync().Result;
|
||||
foreach (var diag in diags)
|
||||
foreach (Diagnostic diag in diags)
|
||||
{
|
||||
if (diag.Location == Location.None || diag.Location.IsInMetadata)
|
||||
{
|
||||
|
@ -71,8 +73,8 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
|
|||
{
|
||||
for (int i = 0; i < documents.Length; i++)
|
||||
{
|
||||
var document = documents[i];
|
||||
var tree = document.GetSyntaxTreeAsync().Result;
|
||||
Document document = documents[i];
|
||||
SyntaxTree? tree = document.GetSyntaxTreeAsync().Result;
|
||||
if (tree == diag.Location.SourceTree)
|
||||
{
|
||||
diagnostics.Add(diag);
|
||||
|
@ -113,7 +115,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
|
|||
throw new ArgumentException("Unsupported Language");
|
||||
}
|
||||
|
||||
var project = CreateProject(sources, language);
|
||||
Project project = CreateProject(sources, language);
|
||||
var documents = project.Documents.ToArray();
|
||||
|
||||
if (sources.Length != documents.Length)
|
||||
|
@ -124,17 +126,6 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
|
|||
return documents;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a Document from a string through creating a project that contains it.
|
||||
/// </summary>
|
||||
/// <param name="source">Classes in the form of a string</param>
|
||||
/// <param name="language">The language the source code is in</param>
|
||||
/// <returns>A Document created from the source string</returns>
|
||||
protected static Document CreateDocument(string source, string language = LanguageNames.CSharp)
|
||||
{
|
||||
return CreateProject(new[] { source }, language).Documents.First();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a project using the inputted strings as sources.
|
||||
/// </summary>
|
||||
|
@ -146,9 +137,9 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
|
|||
string fileNamePrefix = DefaultFilePathPrefix;
|
||||
string fileExt = language == LanguageNames.CSharp ? CSharpDefaultFileExt : VisualBasicDefaultExt;
|
||||
|
||||
var projectId = ProjectId.CreateNewId(debugName: TestProjectName);
|
||||
ProjectId projectId = ProjectId.CreateNewId(debugName: TestProjectName);
|
||||
|
||||
var solution = new AdhocWorkspace()
|
||||
Solution solution = new AdhocWorkspace()
|
||||
.CurrentSolution
|
||||
.AddProject(projectId, TestProjectName, TestProjectName, language)
|
||||
.AddMetadataReference(projectId, DiagnosticVerifier.SelfReference)
|
||||
|
@ -158,14 +149,14 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
|
|||
.AddMetadataReference(projectId, CodeAnalysisReference);
|
||||
|
||||
int count = 0;
|
||||
foreach (var source in sources)
|
||||
foreach (string source in sources)
|
||||
{
|
||||
var newFileName = fileNamePrefix + count + "." + fileExt;
|
||||
var documentId = DocumentId.CreateNewId(projectId, debugName: newFileName);
|
||||
string newFileName = fileNamePrefix + count + "." + fileExt;
|
||||
DocumentId documentId = DocumentId.CreateNewId(projectId, debugName: newFileName);
|
||||
solution = solution.AddDocument(documentId, newFileName, SourceText.From(source));
|
||||
count++;
|
||||
}
|
||||
return solution.GetProject(projectId);
|
||||
return solution.GetProject(projectId)!;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
// <generated />
|
||||
// ReSharper disable All -- generated code
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
@ -17,18 +19,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
|
|||
/// <summary>
|
||||
/// Get the CSharp analyzer being tested - to be implemented in non-abstract class
|
||||
/// </summary>
|
||||
protected virtual DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the Visual Basic analyzer being tested (C#) - to be implemented in non-abstract class
|
||||
/// </summary>
|
||||
protected virtual DiagnosticAnalyzer GetBasicDiagnosticAnalyzer()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
protected abstract DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer();
|
||||
#endregion
|
||||
|
||||
#region Verifier wrappers
|
||||
|
@ -41,18 +32,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
|
|||
/// <param name="expected"> DiagnosticResults that should appear after the analyzer is run on the source</param>
|
||||
protected void VerifyCSharpDiagnostic(string source, params DiagnosticResult[] expected)
|
||||
{
|
||||
VerifyDiagnostics(new[] { source }, LanguageNames.CSharp, GetCSharpDiagnosticAnalyzer(), expected);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called to test a C# DiagnosticAnalyzer when applied on the inputted strings as a source
|
||||
/// Note: input a DiagnosticResult for each Diagnostic expected
|
||||
/// </summary>
|
||||
/// <param name="sources">An array of strings to create source documents from to run the analyzers on</param>
|
||||
/// <param name="expected">DiagnosticResults that should appear after the analyzer is run on the sources</param>
|
||||
protected void VerifyCSharpDiagnostic(string[] sources, params DiagnosticResult[] expected)
|
||||
{
|
||||
VerifyDiagnostics(sources, LanguageNames.CSharp, GetCSharpDiagnosticAnalyzer(), expected);
|
||||
this.VerifyDiagnostics(new[] { source }, LanguageNames.CSharp, this.GetCSharpDiagnosticAnalyzer(), expected);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -65,8 +45,8 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
|
|||
/// <param name="expected">DiagnosticResults that should appear after the analyzer is run on the sources</param>
|
||||
private void VerifyDiagnostics(string[] sources, string language, DiagnosticAnalyzer analyzer, params DiagnosticResult[] expected)
|
||||
{
|
||||
var diagnostics = GetSortedDiagnostics(sources, language, analyzer);
|
||||
VerifyDiagnosticResults(diagnostics, analyzer, expected);
|
||||
var diagnostics = DiagnosticVerifier.GetSortedDiagnostics(sources, language, analyzer);
|
||||
DiagnosticVerifier.VerifyDiagnosticResults(diagnostics, analyzer, expected);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
@ -86,7 +66,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
|
|||
|
||||
if (expectedCount != actualCount)
|
||||
{
|
||||
string diagnosticsOutput = actualResults.Any() ? FormatDiagnostics(analyzer, actualResults.ToArray()) : " NONE.";
|
||||
string diagnosticsOutput = actualResults.Any() ? DiagnosticVerifier.FormatDiagnostics(analyzer, actualResults.ToArray()) : " NONE.";
|
||||
|
||||
Assert.IsTrue(false,
|
||||
string.Format("Mismatch between number of diagnostics returned, expected \"{0}\" actual \"{1}\"\r\n\r\nDiagnostics:\r\n{2}\r\n", expectedCount, actualCount, diagnosticsOutput));
|
||||
|
@ -103,12 +83,12 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
|
|||
{
|
||||
Assert.IsTrue(false,
|
||||
string.Format("Expected:\nA project diagnostic with No location\nActual:\n{0}",
|
||||
FormatDiagnostics(analyzer, actual)));
|
||||
DiagnosticVerifier.FormatDiagnostics(analyzer, actual)));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
VerifyDiagnosticLocation(analyzer, actual, actual.Location, expected.Locations.First());
|
||||
DiagnosticVerifier.VerifyDiagnosticLocation(analyzer, actual, actual.Location, expected.Locations.First());
|
||||
var additionalLocations = actual.AdditionalLocations.ToArray();
|
||||
|
||||
if (additionalLocations.Length != expected.Locations.Length - 1)
|
||||
|
@ -116,12 +96,12 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
|
|||
Assert.IsTrue(false,
|
||||
string.Format("Expected {0} additional locations but got {1} for Diagnostic:\r\n {2}\r\n",
|
||||
expected.Locations.Length - 1, additionalLocations.Length,
|
||||
FormatDiagnostics(analyzer, actual)));
|
||||
DiagnosticVerifier.FormatDiagnostics(analyzer, actual)));
|
||||
}
|
||||
|
||||
for (int j = 0; j < additionalLocations.Length; ++j)
|
||||
{
|
||||
VerifyDiagnosticLocation(analyzer, actual, additionalLocations[j], expected.Locations[j + 1]);
|
||||
DiagnosticVerifier.VerifyDiagnosticLocation(analyzer, actual, additionalLocations[j], expected.Locations[j + 1]);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -129,21 +109,21 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
|
|||
{
|
||||
Assert.IsTrue(false,
|
||||
string.Format("Expected diagnostic id to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n",
|
||||
expected.Id, actual.Id, FormatDiagnostics(analyzer, actual)));
|
||||
expected.Id, actual.Id, DiagnosticVerifier.FormatDiagnostics(analyzer, actual)));
|
||||
}
|
||||
|
||||
if (actual.Severity != expected.Severity)
|
||||
{
|
||||
Assert.IsTrue(false,
|
||||
string.Format("Expected diagnostic severity to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n",
|
||||
expected.Severity, actual.Severity, FormatDiagnostics(analyzer, actual)));
|
||||
expected.Severity, actual.Severity, DiagnosticVerifier.FormatDiagnostics(analyzer, actual)));
|
||||
}
|
||||
|
||||
if (actual.GetMessage() != expected.Message)
|
||||
{
|
||||
Assert.IsTrue(false,
|
||||
string.Format("Expected diagnostic message to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n",
|
||||
expected.Message, actual.GetMessage(), FormatDiagnostics(analyzer, actual)));
|
||||
expected.Message, actual.GetMessage(), DiagnosticVerifier.FormatDiagnostics(analyzer, actual)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -161,7 +141,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
|
|||
|
||||
Assert.IsTrue(actualSpan.Path == expected.Path || (actualSpan.Path != null && actualSpan.Path.Contains("Test0.") && expected.Path.Contains("Test.")),
|
||||
string.Format("Expected diagnostic to be in file \"{0}\" was actually in file \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n",
|
||||
expected.Path, actualSpan.Path, FormatDiagnostics(analyzer, diagnostic)));
|
||||
expected.Path, actualSpan.Path, DiagnosticVerifier.FormatDiagnostics(analyzer, diagnostic)));
|
||||
|
||||
var actualLinePosition = actualSpan.StartLinePosition;
|
||||
|
||||
|
@ -172,7 +152,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
|
|||
{
|
||||
Assert.IsTrue(false,
|
||||
string.Format("Expected diagnostic to be on line \"{0}\" was actually on line \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n",
|
||||
expected.Line, actualLinePosition.Line + 1, FormatDiagnostics(analyzer, diagnostic)));
|
||||
expected.Line, actualLinePosition.Line + 1, DiagnosticVerifier.FormatDiagnostics(analyzer, diagnostic)));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -183,7 +163,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
|
|||
{
|
||||
Assert.IsTrue(false,
|
||||
string.Format("Expected diagnostic to start at column \"{0}\" was actually at column \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n",
|
||||
expected.Column, actualLinePosition.Character + 1, FormatDiagnostics(analyzer, diagnostic)));
|
||||
expected.Column, actualLinePosition.Character + 1, DiagnosticVerifier.FormatDiagnostics(analyzer, diagnostic)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -201,7 +181,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
|
|||
var builder = new StringBuilder();
|
||||
for (int i = 0; i < diagnostics.Length; ++i)
|
||||
{
|
||||
builder.AppendLine("// " + diagnostics[i].ToString());
|
||||
builder.AppendLine("// " + diagnostics[i]);
|
||||
|
||||
var analyzerType = analyzer.GetType();
|
||||
var rules = analyzer.SupportedDiagnostics;
|
||||
|
@ -220,11 +200,10 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests.Framework
|
|||
Assert.IsTrue(location.IsInSource,
|
||||
$"Test base does not currently handle diagnostics in metadata locations. Diagnostic in metadata: {diagnostics[i]}\r\n");
|
||||
|
||||
string resultMethodName = diagnostics[i].Location.SourceTree.FilePath.EndsWith(".cs") ? "GetCSharpResultAt" : "GetBasicResultAt";
|
||||
var linePosition = diagnostics[i].Location.GetLineSpan().StartLinePosition;
|
||||
|
||||
builder.AppendFormat("{0}({1}, {2}, {3}.{4})",
|
||||
resultMethodName,
|
||||
"GetCSharpResultAt",
|
||||
linePosition.Line + 1,
|
||||
linePosition.Character + 1,
|
||||
analyzerType.Name,
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
// ReSharper disable CheckNamespace -- matches Stardew Valley's code
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Netcode
|
||||
{
|
||||
/// <summary>A simplified version of Stardew Valley's <c>Netcode.NetCollection</c> for unit testing.</summary>
|
||||
public class NetCollection<T> : Collection<T>, IList<T>, ICollection<T>, IEnumerable<T>, IEnumerable { }
|
||||
public class NetCollection<T> : Collection<T> { }
|
||||
}
|
||||
|
|
|
@ -7,10 +7,13 @@ namespace Netcode
|
|||
public class NetFieldBase<T, TSelf> where TSelf : NetFieldBase<T, TSelf>
|
||||
{
|
||||
/// <summary>The synchronised value.</summary>
|
||||
public T Value { get; set; }
|
||||
public T? Value { get; set; }
|
||||
|
||||
/// <summary>Implicitly convert a net field to the its type.</summary>
|
||||
/// <param name="field">The field to convert.</param>
|
||||
public static implicit operator T(NetFieldBase<T, TSelf> field) => field.Value;
|
||||
public static implicit operator T?(NetFieldBase<T, TSelf> field)
|
||||
{
|
||||
return field.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
// ReSharper disable CheckNamespace -- matches Stardew Valley's code
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Netcode
|
||||
{
|
||||
/// <summary>A simplified version of Stardew Valley's <c>Netcode.NetObjectList</c> for unit testing.</summary>
|
||||
public class NetList<T> : List<T>, IList<T>, ICollection<T>, IEnumerable<T>, IEnumerable { }
|
||||
public class NetList<T> : List<T> { }
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// ReSharper disable CheckNamespace, InconsistentNaming -- matches Stardew Valley's code
|
||||
#pragma warning disable 649 // (never assigned) -- only used to test type conversions
|
||||
// ReSharper disable UnusedMember.Global -- used dynamically for unit tests
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace StardewValley
|
||||
|
@ -8,6 +8,6 @@ namespace StardewValley
|
|||
internal class Farmer
|
||||
{
|
||||
/// <summary>A sample field which should be replaced with a different property.</summary>
|
||||
public readonly IDictionary<string, int[]> friendships;
|
||||
public readonly IDictionary<string, int[]> friendships = new Dictionary<string, int[]>();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
// ReSharper disable CheckNamespace, InconsistentNaming -- matches Stardew Valley's code
|
||||
// ReSharper disable UnusedMember.Global -- used dynamically for unit tests
|
||||
using Netcode;
|
||||
|
||||
namespace StardewValley
|
||||
|
@ -7,27 +8,27 @@ namespace StardewValley
|
|||
public class Item
|
||||
{
|
||||
/// <summary>A net int field with an equivalent non-net <c>Category</c> property.</summary>
|
||||
public readonly NetInt category = new NetInt { Value = 42 };
|
||||
public readonly NetInt category = new() { Value = 42 };
|
||||
|
||||
/// <summary>A generic net int field with no equivalent non-net property.</summary>
|
||||
public readonly NetInt netIntField = new NetInt { Value = 42 };
|
||||
public readonly NetInt netIntField = new() { Value = 42 };
|
||||
|
||||
/// <summary>A generic net ref field with no equivalent non-net property.</summary>
|
||||
public readonly NetRef<object> netRefField = new NetRef<object>();
|
||||
public readonly NetRef<object> netRefField = new();
|
||||
|
||||
/// <summary>A generic net int property with no equivalent non-net property.</summary>
|
||||
public NetInt netIntProperty = new NetInt { Value = 42 };
|
||||
public NetInt netIntProperty = new() { Value = 42 };
|
||||
|
||||
/// <summary>A generic net ref property with no equivalent non-net property.</summary>
|
||||
public NetRef<object> netRefProperty { get; } = new NetRef<object>();
|
||||
public NetRef<object> netRefProperty { get; } = new();
|
||||
|
||||
/// <summary>A sample net list.</summary>
|
||||
public readonly NetList<int> netList = new NetList<int>();
|
||||
public readonly NetList<int> netList = new();
|
||||
|
||||
/// <summary>A sample net object list.</summary>
|
||||
public readonly NetObjectList<int> netObjectList = new NetObjectList<int>();
|
||||
public readonly NetObjectList<int> netObjectList = new();
|
||||
|
||||
/// <summary>A sample net collection.</summary>
|
||||
public readonly NetCollection<int> netCollection = new NetCollection<int>();
|
||||
public readonly NetCollection<int> netCollection = new();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,6 @@ namespace StardewValley
|
|||
public class Object : Item
|
||||
{
|
||||
/// <summary>A net int field with an equivalent non-net property.</summary>
|
||||
public NetInt type = new NetInt { Value = 42 };
|
||||
public NetInt type = new() { Value = 42 };
|
||||
}
|
||||
}
|
||||
|
|
|
@ -87,13 +87,13 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests
|
|||
[TestCase("SObject obj = null; if (obj.netRefProperty != null);", 24, "obj.netRefProperty", "NetRef", "object")]
|
||||
[TestCase("Item item = new Item(); object list = item.netList;", 38, "item.netList", "NetList", "object")] // ↓ NetList field converted to a non-interface type
|
||||
[TestCase("Item item = new Item(); object list = item.netCollection;", 38, "item.netCollection", "NetCollection", "object")]
|
||||
[TestCase("Item item = new Item(); int x = (int)item.netIntField;", 32, "item.netIntField", "NetInt", "int")] // ↓ explicit conversion to invalid type
|
||||
[TestCase("Item item = new Item(); int x = (int)item.netIntField;", 32, "item.netIntField", "NetFieldBase", "int")] // ↓ explicit conversion to invalid type
|
||||
[TestCase("Item item = new Item(); int x = item.netRefField as object;", 32, "item.netRefField", "NetRef", "object")]
|
||||
public void AvoidImplicitNetFieldComparisons_RaisesDiagnostic(string codeText, int column, string expression, string fromType, string toType)
|
||||
{
|
||||
// arrange
|
||||
string code = NetFieldAnalyzerTests.SampleProgram.Replace("{{test-code}}", codeText);
|
||||
DiagnosticResult expected = new DiagnosticResult
|
||||
DiagnosticResult expected = new()
|
||||
{
|
||||
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/package/avoid-implicit-net-field-cast for details.",
|
||||
|
@ -135,7 +135,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests
|
|||
{
|
||||
// arrange
|
||||
string code = NetFieldAnalyzerTests.SampleProgram.Replace("{{test-code}}", codeText);
|
||||
DiagnosticResult expected = new DiagnosticResult
|
||||
DiagnosticResult expected = new()
|
||||
{
|
||||
Id = "AvoidNetField",
|
||||
Message = $"'{expression}' is a {netType} field; consider using the {suggestedProperty} property instead. See https://smapi.io/package/avoid-net-field for details.",
|
||||
|
|
|
@ -64,7 +64,7 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests
|
|||
{
|
||||
// arrange
|
||||
string code = ObsoleteFieldAnalyzerTests.SampleProgram.Replace("{{test-code}}", codeText);
|
||||
DiagnosticResult expected = new DiagnosticResult
|
||||
DiagnosticResult expected = new()
|
||||
{
|
||||
Id = "AvoidObsoleteField",
|
||||
Message = $"The '{oldName}' field is obsolete and should be replaced with '{newName}'. See https://smapi.io/package/avoid-obsolete-field for details.",
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
## Release 2.1.0
|
||||
### New Rules
|
||||
Rule ID | Category | Severity | Notes
|
||||
------------------------- | ------------------ | -------- | ------------------------------------------------------------
|
||||
AvoidImplicitNetFieldCast | SMAPI.CommonErrors | Warning | See [documentation](https://smapi.io/package/code-warnings).
|
||||
AvoidNetField | SMAPI.CommonErrors | Warning | See [documentation](https://smapi.io/package/code-warnings).
|
||||
AvoidObsoleteField | SMAPI.CommonErrors | Warning | See [documentation](https://smapi.io/package/code-warnings).
|
|
@ -40,8 +40,8 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer
|
|||
|
||||
// invalid
|
||||
fromExpression = null;
|
||||
fromType = default(TypeInfo);
|
||||
toType = default(TypeInfo);
|
||||
fromType = default;
|
||||
toType = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -64,7 +64,7 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer
|
|||
}
|
||||
|
||||
// conditional access
|
||||
if (node is ConditionalAccessExpressionSyntax conditionalAccess && conditionalAccess.WhenNotNull is MemberBindingExpressionSyntax conditionalBinding)
|
||||
if (node is ConditionalAccessExpressionSyntax { WhenNotNull: MemberBindingExpressionSyntax conditionalBinding } conditionalAccess)
|
||||
{
|
||||
declaringType = semanticModel.GetTypeInfo(conditionalAccess.Expression).Type;
|
||||
memberType = semanticModel.GetTypeInfo(node);
|
||||
|
@ -74,7 +74,7 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer
|
|||
|
||||
// invalid
|
||||
declaringType = null;
|
||||
memberType = default(TypeInfo);
|
||||
memberType = default;
|
||||
memberName = null;
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -132,7 +132,7 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer
|
|||
};
|
||||
|
||||
/// <summary>The diagnostic info for an implicit net field cast.</summary>
|
||||
private readonly DiagnosticDescriptor AvoidImplicitNetFieldCastRule = new DiagnosticDescriptor(
|
||||
private readonly DiagnosticDescriptor AvoidImplicitNetFieldCastRule = new(
|
||||
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/package/avoid-implicit-net-field-cast for details.",
|
||||
|
@ -143,7 +143,7 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer
|
|||
);
|
||||
|
||||
/// <summary>The diagnostic info for an avoidable net field access.</summary>
|
||||
private readonly DiagnosticDescriptor AvoidNetFieldRule = new DiagnosticDescriptor(
|
||||
private readonly DiagnosticDescriptor AvoidNetFieldRule = new(
|
||||
id: "AvoidNetField",
|
||||
title: "Avoid Netcode types when possible",
|
||||
messageFormat: "'{0}' is a {1} field; consider using the {2} property instead. See https://smapi.io/package/avoid-net-field for details.",
|
||||
|
@ -227,10 +227,7 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer
|
|||
|
||||
// warn: implicit conversion
|
||||
if (this.IsInvalidConversion(memberType.Type, memberType.ConvertedType))
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(this.AvoidImplicitNetFieldCastRule, context.Node.GetLocation(), context.Node, memberType.Type.Name, memberType.ConvertedType));
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer
|
|||
/// <summary>Describes the diagnostic rule covered by the analyzer.</summary>
|
||||
private readonly IDictionary<string, DiagnosticDescriptor> Rules = new Dictionary<string, DiagnosticDescriptor>
|
||||
{
|
||||
["AvoidObsoleteField"] = new DiagnosticDescriptor(
|
||||
["AvoidObsoleteField"] = new(
|
||||
id: "AvoidObsoleteField",
|
||||
title: "Reference to obsolete field",
|
||||
messageFormat: "The '{0}' field is obsolete and should be replaced with '{1}'. See https://smapi.io/package/avoid-obsolete-field for details.",
|
||||
|
@ -77,7 +77,7 @@ namespace StardewModdingAPI.ModBuildConfig.Analyzer
|
|||
try
|
||||
{
|
||||
// get reference info
|
||||
if (!AnalyzerUtilities.TryGetMemberInfo(context.Node, context.SemanticModel, out ITypeSymbol declaringType, out TypeInfo memberType, out string memberName))
|
||||
if (!AnalyzerUtilities.TryGetMemberInfo(context.Node, context.SemanticModel, out ITypeSymbol declaringType, out _, out string memberName))
|
||||
return;
|
||||
|
||||
// suggest replacement
|
||||
|
|
|
@ -12,6 +12,10 @@
|
|||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.10.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="AnalyzerReleases.Shipped.md" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
|
||||
</ItemGroup>
|
||||
|
|
|
@ -88,7 +88,7 @@ namespace StardewModdingAPI.ModBuildConfig
|
|||
Regex[] ignoreFilePatterns = this.GetCustomIgnorePatterns().ToArray();
|
||||
|
||||
// get mod info
|
||||
ModFileManager package = new ModFileManager(this.ProjectDir, this.TargetDir, ignoreFilePaths, ignoreFilePatterns, bundleAssemblyTypes, this.ModDllName, validateRequiredModFiles: this.EnableModDeploy || this.EnableModZip);
|
||||
ModFileManager package = new(this.ProjectDir, this.TargetDir, ignoreFilePaths, ignoreFilePatterns, bundleAssemblyTypes, this.ModDllName, validateRequiredModFiles: this.EnableModDeploy || this.EnableModZip);
|
||||
|
||||
// deploy mod files
|
||||
if (this.EnableModDeploy)
|
||||
|
@ -246,7 +246,7 @@ namespace StardewModdingAPI.ModBuildConfig
|
|||
// create zip file
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(zipPath)!);
|
||||
using Stream zipStream = new FileStream(zipPath, FileMode.Create, FileAccess.Write);
|
||||
using ZipArchive archive = new ZipArchive(zipStream, ZipArchiveMode.Create);
|
||||
using ZipArchive archive = new(zipStream, ZipArchiveMode.Create);
|
||||
|
||||
foreach (var fileEntry in files)
|
||||
{
|
||||
|
|
|
@ -136,7 +136,7 @@ namespace StardewModdingAPI.ModBuildConfig.Framework
|
|||
// project manifest
|
||||
bool hasProjectManifest = false;
|
||||
{
|
||||
FileInfo manifest = new FileInfo(Path.Combine(projectDir, this.ManifestFileName));
|
||||
FileInfo manifest = new(Path.Combine(projectDir, this.ManifestFileName));
|
||||
if (manifest.Exists)
|
||||
{
|
||||
yield return Tuple.Create(this.ManifestFileName, manifest);
|
||||
|
@ -146,7 +146,7 @@ namespace StardewModdingAPI.ModBuildConfig.Framework
|
|||
|
||||
// project i18n files
|
||||
bool hasProjectTranslations = false;
|
||||
DirectoryInfo translationsFolder = new DirectoryInfo(Path.Combine(projectDir, "i18n"));
|
||||
DirectoryInfo translationsFolder = new(Path.Combine(projectDir, "i18n"));
|
||||
if (translationsFolder.Exists)
|
||||
{
|
||||
foreach (FileInfo file in translationsFolder.EnumerateFiles())
|
||||
|
@ -156,7 +156,7 @@ namespace StardewModdingAPI.ModBuildConfig.Framework
|
|||
|
||||
// project assets folder
|
||||
bool hasAssetsFolder = false;
|
||||
DirectoryInfo assetsFolder = new DirectoryInfo(Path.Combine(projectDir, "assets"));
|
||||
DirectoryInfo assetsFolder = new(Path.Combine(projectDir, "assets"));
|
||||
if (assetsFolder.Exists)
|
||||
{
|
||||
foreach (FileInfo file in assetsFolder.EnumerateFiles("*", SearchOption.AllDirectories))
|
||||
|
@ -168,7 +168,7 @@ namespace StardewModdingAPI.ModBuildConfig.Framework
|
|||
}
|
||||
|
||||
// build output
|
||||
DirectoryInfo buildFolder = new DirectoryInfo(targetDir);
|
||||
DirectoryInfo buildFolder = new(targetDir);
|
||||
foreach (FileInfo file in buildFolder.EnumerateFiles("*", SearchOption.AllDirectories))
|
||||
{
|
||||
// get path info
|
||||
|
|
|
@ -5,11 +5,12 @@
|
|||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
|
||||
|
||||
<!--NuGet package-->
|
||||
<PackageId>Pathoschild.Stardew.ModBuildConfig</PackageId>
|
||||
<Title>Build package for SMAPI mods</Title>
|
||||
<Version>4.0.0</Version>
|
||||
<Version>4.0.1</Version>
|
||||
<Authors>Pathoschild</Authors>
|
||||
<Description>Automates the build configuration for crossplatform Stardew Valley SMAPI mods. For SMAPI 3.13.0 or later.</Description>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
|
@ -24,6 +25,12 @@
|
|||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Build.Utilities.Core" Version="16.10" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
|
||||
<!--
|
||||
This is imported through Microsoft.Build.Utilities.Core. When installed by a mod, NuGet
|
||||
otherwise imports version 4.3.0 instead of 5.0.0, which conflicts with SMAPI's version.
|
||||
-->
|
||||
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
|
||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands
|
||||
|
@ -52,7 +53,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands
|
|||
/// <param name="value">The parsed value.</param>
|
||||
/// <param name="required">Whether to show an error if the argument is missing.</param>
|
||||
/// <param name="oneOf">Require that the argument match one of the given values (case-insensitive).</param>
|
||||
public bool TryGet(int index, string name, out string value, bool required = true, string[] oneOf = null)
|
||||
public bool TryGet(int index, string name, [NotNullWhen(true)] out string? value, bool required = true, string[]? oneOf = null)
|
||||
{
|
||||
value = null;
|
||||
|
||||
|
@ -86,7 +87,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands
|
|||
value = 0;
|
||||
|
||||
// get argument
|
||||
if (!this.TryGet(index, name, out string raw, required))
|
||||
if (!this.TryGet(index, name, out string? raw, required))
|
||||
return false;
|
||||
|
||||
// parse
|
||||
|
|
|
@ -100,7 +100,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands
|
|||
List<string[]> lines = new List<string[]>(rows.Length + 2)
|
||||
{
|
||||
header,
|
||||
header.Select((value, i) => "".PadRight(widths[i], '-')).ToArray()
|
||||
header.Select((_, i) => "".PadRight(widths[i], '-')).ToArray()
|
||||
};
|
||||
lines.AddRange(rows);
|
||||
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using StardewValley;
|
||||
|
||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other
|
||||
{
|
||||
/// <summary>A command which runs one of the game's save migrations.</summary>
|
||||
[SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
|
||||
internal class ApplySaveFixCommand : ConsoleCommand
|
||||
{
|
||||
/*********
|
||||
|
@ -21,7 +23,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other
|
|||
public override void Handle(IMonitor monitor, string command, ArgumentParser args)
|
||||
{
|
||||
// get fix ID
|
||||
if (!args.TryGet(0, "fix_id", out string rawFixId, required: false))
|
||||
if (!args.TryGet(0, "fix_id", out string? rawFixId, required: false))
|
||||
{
|
||||
monitor.Log("Invalid usage. Type 'help apply_save_fix' for details.", LogLevel.Error);
|
||||
return;
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
using StardewValley;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using StardewValley;
|
||||
|
||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other
|
||||
{
|
||||
/// <summary>A command which sends a debug command to the game.</summary>
|
||||
[SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
|
||||
internal class DebugCommand : ConsoleCommand
|
||||
{
|
||||
/*********
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Netcode;
|
||||
|
@ -9,6 +10,7 @@ using StardewValley.Network;
|
|||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other
|
||||
{
|
||||
/// <summary>A command which regenerates the game's bundles.</summary>
|
||||
[SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
|
||||
internal class RegenerateBundlesCommand : ConsoleCommand
|
||||
{
|
||||
/*********
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
using System.Diagnostics;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other
|
||||
{
|
||||
/// <summary>A command which shows the data files.</summary>
|
||||
[SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
|
||||
internal class ShowDataFilesCommand : ConsoleCommand
|
||||
{
|
||||
/*********
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
using System.Diagnostics;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other
|
||||
{
|
||||
/// <summary>A command which shows the game files.</summary>
|
||||
[SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
|
||||
internal class ShowGameFilesCommand : ConsoleCommand
|
||||
{
|
||||
/*********
|
||||
|
@ -18,8 +20,8 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other
|
|||
/// <param name="args">The command arguments.</param>
|
||||
public override void Handle(IMonitor monitor, string command, ArgumentParser args)
|
||||
{
|
||||
Process.Start(Constants.ExecutionPath);
|
||||
monitor.Log($"OK, opening {Constants.ExecutionPath}.", LogLevel.Info);
|
||||
Process.Start(Constants.GamePath);
|
||||
monitor.Log($"OK, opening {Constants.GamePath}.", LogLevel.Info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other
|
||||
{
|
||||
/// <summary>A command which logs the keys being pressed for 30 seconds once enabled.</summary>
|
||||
[SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
|
||||
internal class TestInputCommand : ConsoleCommand
|
||||
{
|
||||
/*********
|
||||
|
@ -37,9 +39,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other
|
|||
public override void OnUpdated(IMonitor monitor)
|
||||
{
|
||||
// handle expiry
|
||||
if (this.ExpiryTicks == null)
|
||||
return;
|
||||
if (this.ExpiryTicks <= DateTime.UtcNow.Ticks)
|
||||
if (this.ExpiryTicks != null && this.ExpiryTicks <= DateTime.UtcNow.Ticks)
|
||||
{
|
||||
monitor.Log("No longer logging input.", LogLevel.Info);
|
||||
this.ExpiryTicks = null;
|
||||
|
|
|
@ -13,7 +13,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
|||
** Fields
|
||||
*********/
|
||||
/// <summary>Provides methods for searching and constructing items.</summary>
|
||||
private readonly ItemRepository Items = new ItemRepository();
|
||||
private readonly ItemRepository Items = new();
|
||||
|
||||
/// <summary>The type names recognized by this command.</summary>
|
||||
private readonly string[] ValidTypes = Enum.GetNames(typeof(ItemType)).Concat(new[] { "Name" }).ToArray();
|
||||
|
@ -40,7 +40,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
|||
}
|
||||
|
||||
// read arguments
|
||||
if (!args.TryGet(0, "item type", out string type, oneOf: this.ValidTypes))
|
||||
if (!args.TryGet(0, "item type", out string? type, oneOf: this.ValidTypes))
|
||||
return;
|
||||
if (!args.TryGetInt(2, "count", out int count, min: 1, required: false))
|
||||
count = 1;
|
||||
|
@ -48,7 +48,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
|||
quality = Object.lowQuality;
|
||||
|
||||
// find matching item
|
||||
SearchableItem match = Enum.TryParse(type, true, out ItemType itemType)
|
||||
SearchableItem? match = Enum.TryParse(type, true, out ItemType itemType)
|
||||
? this.FindItemByID(monitor, args, itemType)
|
||||
: this.FindItemByName(monitor, args);
|
||||
if (match == null)
|
||||
|
@ -76,14 +76,14 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
|||
/// <param name="monitor">Writes messages to the console and log file.</param>
|
||||
/// <param name="args">The command arguments.</param>
|
||||
/// <param name="type">The item type.</param>
|
||||
private SearchableItem FindItemByID(IMonitor monitor, ArgumentParser args, ItemType type)
|
||||
private SearchableItem? FindItemByID(IMonitor monitor, ArgumentParser args, ItemType type)
|
||||
{
|
||||
// read arguments
|
||||
if (!args.TryGetInt(1, "item ID", out int id, min: 0))
|
||||
return null;
|
||||
|
||||
// find matching item
|
||||
SearchableItem item = this.Items.GetAll().FirstOrDefault(p => p.Type == type && p.ID == id);
|
||||
SearchableItem? item = this.Items.GetAll().FirstOrDefault(p => p.Type == type && p.ID == id);
|
||||
if (item == null)
|
||||
monitor.Log($"There's no {type} item with ID {id}.", LogLevel.Error);
|
||||
return item;
|
||||
|
@ -92,10 +92,10 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
|||
/// <summary>Get a matching item by its name.</summary>
|
||||
/// <param name="monitor">Writes messages to the console and log file.</param>
|
||||
/// <param name="args">The command arguments.</param>
|
||||
private SearchableItem FindItemByName(IMonitor monitor, ArgumentParser args)
|
||||
private SearchableItem? FindItemByName(IMonitor monitor, ArgumentParser args)
|
||||
{
|
||||
// read arguments
|
||||
if (!args.TryGet(1, "item name", out string name))
|
||||
if (!args.TryGet(1, "item name", out string? name))
|
||||
return null;
|
||||
|
||||
// find matching items
|
||||
|
|
|
@ -1,16 +1,18 @@
|
|||
using System.Linq;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData;
|
||||
|
||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
||||
{
|
||||
/// <summary>A command which list item types.</summary>
|
||||
[SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
|
||||
internal class ListItemTypesCommand : ConsoleCommand
|
||||
{
|
||||
/*********
|
||||
** Fields
|
||||
*********/
|
||||
/// <summary>Provides methods for searching and constructing items.</summary>
|
||||
private readonly ItemRepository Items = new ItemRepository();
|
||||
private readonly ItemRepository Items = new();
|
||||
|
||||
|
||||
/*********
|
||||
|
|
|
@ -1,18 +1,20 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData;
|
||||
|
||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
||||
{
|
||||
/// <summary>A command which list items available to spawn.</summary>
|
||||
[SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
|
||||
internal class ListItemsCommand : ConsoleCommand
|
||||
{
|
||||
/*********
|
||||
** Fields
|
||||
*********/
|
||||
/// <summary>Provides methods for searching and constructing items.</summary>
|
||||
private readonly ItemRepository Items = new ItemRepository();
|
||||
private readonly ItemRepository Items = new();
|
||||
|
||||
|
||||
/*********
|
||||
|
@ -59,15 +61,14 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
|||
private IEnumerable<SearchableItem> GetItems(string[] searchWords)
|
||||
{
|
||||
// normalize search term
|
||||
searchWords = searchWords?.Where(word => !string.IsNullOrWhiteSpace(word)).ToArray();
|
||||
if (searchWords?.Any() == false)
|
||||
searchWords = null;
|
||||
searchWords = searchWords.Where(word => !string.IsNullOrWhiteSpace(word)).ToArray();
|
||||
bool getAll = !searchWords.Any();
|
||||
|
||||
// find matches
|
||||
return (
|
||||
from item in this.Items.GetAll()
|
||||
let term = $"{item.ID}|{item.Type}|{item.Name}|{item.DisplayName}"
|
||||
where searchWords == null || searchWords.All(word => term.IndexOf(word, StringComparison.CurrentCultureIgnoreCase) != -1)
|
||||
where getAll || searchWords.All(word => term.IndexOf(word, StringComparison.CurrentCultureIgnoreCase) != -1)
|
||||
select item
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Xna.Framework;
|
||||
using StardewValley;
|
||||
|
||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
||||
{
|
||||
/// <summary>A command which edits the color of a player feature.</summary>
|
||||
[SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
|
||||
internal class SetColorCommand : ConsoleCommand
|
||||
{
|
||||
/*********
|
||||
|
@ -20,9 +22,9 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
|||
public override void Handle(IMonitor monitor, string command, ArgumentParser args)
|
||||
{
|
||||
// parse arguments
|
||||
if (!args.TryGet(0, "target", out string target, oneOf: new[] { "hair", "eyes", "pants" }))
|
||||
if (!args.TryGet(0, "target", out string? target, oneOf: new[] { "hair", "eyes", "pants" }))
|
||||
return;
|
||||
if (!args.TryGet(1, "color", out string rawColor))
|
||||
if (!args.TryGet(1, "color", out string? rawColor))
|
||||
return;
|
||||
|
||||
// parse color
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
@ -10,6 +11,7 @@ using StardewValley.GameData;
|
|||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
||||
{
|
||||
/// <summary>A command which changes the player's farm type.</summary>
|
||||
[SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
|
||||
internal class SetFarmTypeCommand : ConsoleCommand
|
||||
{
|
||||
/*********
|
||||
|
@ -33,7 +35,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
|||
}
|
||||
|
||||
// parse arguments
|
||||
if (!args.TryGet(0, "farm type", out string farmType))
|
||||
if (!args.TryGet(0, "farm type", out string? farmType))
|
||||
return;
|
||||
bool isVanillaId = int.TryParse(farmType, out int vanillaId) && vanillaId is (>= 0 and < Farm.layout_max);
|
||||
|
||||
|
@ -108,7 +110,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
|||
return;
|
||||
}
|
||||
|
||||
if (!this.GetCustomFarmTypes().TryGetValue(id, out ModFarmType customFarmType))
|
||||
if (!this.GetCustomFarmTypes().TryGetValue(id, out ModFarmType? customFarmType))
|
||||
{
|
||||
monitor.Log($"Invalid farm type '{id}'. Enter `help set_farm_type` for more info.", LogLevel.Error);
|
||||
return;
|
||||
|
@ -121,7 +123,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
|||
/// <summary>Change the farm type.</summary>
|
||||
/// <param name="type">The farm type ID.</param>
|
||||
/// <param name="customFarmData">The custom farm type data, if applicable.</param>
|
||||
private void SetFarmType(int type, ModFarmType customFarmData)
|
||||
private void SetFarmType(int type, ModFarmType? customFarmData)
|
||||
{
|
||||
// set flags
|
||||
Game1.whichFarm = type;
|
||||
|
@ -131,9 +133,10 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
|||
Farm farm = Game1.getFarm();
|
||||
farm.mapPath.Value = $@"Maps\{Farm.getMapNameFromTypeInt(Game1.whichFarm)}";
|
||||
farm.reloadMap();
|
||||
farm.updateWarps();
|
||||
|
||||
// clear spouse area cache to avoid errors
|
||||
FieldInfo cacheField = farm.GetType().GetField("_baseSpouseAreaTiles", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
||||
FieldInfo? cacheField = farm.GetType().GetField("_baseSpouseAreaTiles", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
||||
if (cacheField == null)
|
||||
throw new InvalidOperationException("Failed to access '_baseSpouseAreaTiles' field to clear spouse area cache.");
|
||||
if (cacheField.GetValue(farm) is not IDictionary cache)
|
||||
|
@ -161,7 +164,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
|||
/// <param name="type">The farm type.</param>
|
||||
private string GetVanillaName(int type)
|
||||
{
|
||||
string translationKey = type switch
|
||||
string? translationKey = type switch
|
||||
{
|
||||
Farm.default_layout => "Character_FarmStandard",
|
||||
Farm.riverlands_layout => "Character_FarmFishing",
|
||||
|
@ -194,7 +197,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
|||
****/
|
||||
/// <summary>Get the display name for a custom farm type.</summary>
|
||||
/// <param name="farmType">The custom farm type.</param>
|
||||
private string GetCustomName(ModFarmType farmType)
|
||||
private string? GetCustomName(ModFarmType? farmType)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(farmType?.TooltipStringPath))
|
||||
return farmType?.ID;
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using StardewValley;
|
||||
|
||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
||||
{
|
||||
/// <summary>A command which edits the player's current health.</summary>
|
||||
[SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
|
||||
internal class SetHealthCommand : ConsoleCommand
|
||||
{
|
||||
/*********
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
using System.Linq;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using StardewValley;
|
||||
|
||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
||||
{
|
||||
/// <summary>A command which edits the player's current immunity.</summary>
|
||||
[SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
|
||||
internal class SetImmunityCommand : ConsoleCommand
|
||||
{
|
||||
/*********
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
using System.Linq;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using StardewValley;
|
||||
|
||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
||||
{
|
||||
/// <summary>A command which edits the player's maximum health.</summary>
|
||||
[SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
|
||||
internal class SetMaxHealthCommand : ConsoleCommand
|
||||
{
|
||||
/*********
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
using System.Linq;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using StardewValley;
|
||||
|
||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
||||
{
|
||||
/// <summary>A command which edits the player's maximum stamina.</summary>
|
||||
[SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
|
||||
internal class SetMaxStaminaCommand : ConsoleCommand
|
||||
{
|
||||
/*********
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using StardewValley;
|
||||
|
||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
||||
{
|
||||
/// <summary>A command which edits the player's current money.</summary>
|
||||
[SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
|
||||
internal class SetMoneyCommand : ConsoleCommand
|
||||
{
|
||||
/*********
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using StardewValley;
|
||||
|
||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
||||
{
|
||||
/// <summary>A command which edits the player's name.</summary>
|
||||
[SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
|
||||
internal class SetNameCommand : ConsoleCommand
|
||||
{
|
||||
/*********
|
||||
|
@ -19,9 +21,9 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
|||
public override void Handle(IMonitor monitor, string command, ArgumentParser args)
|
||||
{
|
||||
// parse arguments
|
||||
if (!args.TryGet(0, "target", out string target, oneOf: new[] { "player", "farm" }))
|
||||
if (!args.TryGet(0, "target", out string? target, oneOf: new[] { "player", "farm" }))
|
||||
return;
|
||||
args.TryGet(1, "name", out string name, required: false);
|
||||
args.TryGet(1, "name", out string? name, required: false);
|
||||
|
||||
// handle
|
||||
switch (target)
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using StardewValley;
|
||||
|
||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
||||
{
|
||||
/// <summary>A command which edits the player's current stamina.</summary>
|
||||
[SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
|
||||
internal class SetStaminaCommand : ConsoleCommand
|
||||
{
|
||||
/*********
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using StardewValley;
|
||||
|
||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
||||
{
|
||||
/// <summary>A command which edits a player style.</summary>
|
||||
[SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
|
||||
internal class SetStyleCommand : ConsoleCommand
|
||||
{
|
||||
/*********
|
||||
|
@ -19,7 +21,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
|
|||
public override void Handle(IMonitor monitor, string command, ArgumentParser args)
|
||||
{
|
||||
// parse arguments
|
||||
if (!args.TryGet(0, "target", out string target, oneOf: new[] { "hair", "shirt", "acc", "skin", "shoe", "swim", "gender" }))
|
||||
if (!args.TryGet(0, "target", out string? target, oneOf: new[] { "hair", "shirt", "acc", "skin", "shoe", "swim", "gender" }))
|
||||
return;
|
||||
if (!args.TryGetInt(1, "style ID", out int styleID))
|
||||
return;
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Microsoft.Xna.Framework;
|
||||
using StardewValley;
|
||||
using StardewValley.Locations;
|
||||
using StardewValley.Objects;
|
||||
|
@ -9,6 +11,7 @@ using SObject = StardewValley.Object;
|
|||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
|
||||
{
|
||||
/// <summary>A command which clears in-game objects.</summary>
|
||||
[SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
|
||||
internal class ClearCommand : ConsoleCommand
|
||||
{
|
||||
/*********
|
||||
|
@ -49,13 +52,13 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
|
|||
}
|
||||
|
||||
// parse arguments
|
||||
if (!args.TryGet(0, "location", out string locationName, required: true))
|
||||
if (!args.TryGet(0, "location", out string? locationName, required: true))
|
||||
return;
|
||||
if (!args.TryGet(1, "object type", out string type, required: true, oneOf: this.ValidTypes))
|
||||
if (!args.TryGet(1, "object type", out string? type, required: true, oneOf: this.ValidTypes))
|
||||
return;
|
||||
|
||||
// get target location
|
||||
GameLocation location = Game1.locations.FirstOrDefault(p => p.Name != null && p.Name.Equals(locationName, StringComparison.OrdinalIgnoreCase));
|
||||
GameLocation? location = Game1.locations.FirstOrDefault(p => p.Name != null && p.Name.Equals(locationName, StringComparison.OrdinalIgnoreCase));
|
||||
if (location == null && locationName == "current")
|
||||
location = Game1.currentLocation;
|
||||
if (location == null)
|
||||
|
@ -92,11 +95,10 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
|
|||
|
||||
removed +=
|
||||
this.RemoveObjects(location, obj =>
|
||||
!(obj is Chest)
|
||||
obj is not Chest
|
||||
&& (
|
||||
obj.Name == "Weeds"
|
||||
|| obj.Name == "Stone"
|
||||
|| (obj.ParentSheetIndex == 294 || obj.ParentSheetIndex == 295)
|
||||
obj.Name is "Weeds" or "Stone"
|
||||
|| obj.ParentSheetIndex is 294 or 295
|
||||
)
|
||||
)
|
||||
+ this.RemoveResourceClumps(location, clump => this.DebrisClumps.Contains(clump.parentSheetIndex.Value));
|
||||
|
@ -114,7 +116,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
|
|||
|
||||
case "furniture":
|
||||
{
|
||||
int removed = this.RemoveFurniture(location, furniture => true);
|
||||
int removed = this.RemoveFurniture(location, _ => true);
|
||||
monitor.Log($"Done! Removed {removed} entities from {location.Name}.", LogLevel.Info);
|
||||
break;
|
||||
}
|
||||
|
@ -138,11 +140,11 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
|
|||
{
|
||||
bool everything = type == "everything";
|
||||
int removed =
|
||||
this.RemoveFurniture(location, p => true)
|
||||
+ this.RemoveObjects(location, p => true)
|
||||
+ this.RemoveTerrainFeatures(location, p => true)
|
||||
+ this.RemoveLargeTerrainFeatures(location, p => everything || !(p is Bush bush) || bush.isDestroyable(location, p.currentTileLocation))
|
||||
+ this.RemoveResourceClumps(location, p => true);
|
||||
this.RemoveFurniture(location, _ => true)
|
||||
+ this.RemoveObjects(location, _ => true)
|
||||
+ this.RemoveTerrainFeatures(location, _ => true)
|
||||
+ this.RemoveLargeTerrainFeatures(location, p => everything || p is not Bush bush || bush.isDestroyable(location, p.currentTileLocation))
|
||||
+ this.RemoveResourceClumps(location, _ => true);
|
||||
monitor.Log($"Done! Removed {removed} entities from {location.Name}.", LogLevel.Info);
|
||||
break;
|
||||
}
|
||||
|
@ -165,11 +167,11 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
|
|||
{
|
||||
int removed = 0;
|
||||
|
||||
foreach (var pair in location.Objects.Pairs.ToArray())
|
||||
foreach ((Vector2 tile, SObject? obj) in location.Objects.Pairs.ToArray())
|
||||
{
|
||||
if (shouldRemove(pair.Value))
|
||||
if (shouldRemove(obj))
|
||||
{
|
||||
location.Objects.Remove(pair.Key);
|
||||
location.Objects.Remove(tile);
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
|
@ -185,11 +187,11 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
|
|||
{
|
||||
int removed = 0;
|
||||
|
||||
foreach (var pair in location.terrainFeatures.Pairs.ToArray())
|
||||
foreach ((Vector2 tile, TerrainFeature? feature) in location.terrainFeatures.Pairs.ToArray())
|
||||
{
|
||||
if (shouldRemove(pair.Value))
|
||||
if (shouldRemove(feature))
|
||||
{
|
||||
location.terrainFeatures.Remove(pair.Key);
|
||||
location.terrainFeatures.Remove(tile);
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
|
@ -225,7 +227,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
|
|||
{
|
||||
int removed = 0;
|
||||
|
||||
foreach (var clump in location.resourceClumps.Where(shouldRemove).ToArray())
|
||||
foreach (ResourceClump clump in location.resourceClumps.Where(shouldRemove).ToArray())
|
||||
{
|
||||
location.resourceClumps.Remove(clump);
|
||||
removed++;
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using StardewValley;
|
||||
using StardewValley.Locations;
|
||||
|
||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
|
||||
{
|
||||
/// <summary>A command which moves the player to the next mine level.</summary>
|
||||
[SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
|
||||
internal class DownMineLevelCommand : ConsoleCommand
|
||||
{
|
||||
/*********
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using StardewValley;
|
||||
|
||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
|
||||
{
|
||||
/// <summary>A command which immediately warps all NPCs to their scheduled positions. To hurry a single NPC, see <c>debug hurry npc-name</c> instead.</summary>
|
||||
[SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
|
||||
internal class HurryAllCommand : ConsoleCommand
|
||||
{
|
||||
/*********
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
using System.Linq;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using StardewModdingAPI.Utilities;
|
||||
using StardewValley;
|
||||
|
||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
|
||||
{
|
||||
/// <summary>A command which sets the current day.</summary>
|
||||
[SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
|
||||
internal class SetDayCommand : ConsoleCommand
|
||||
{
|
||||
/*********
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using StardewValley;
|
||||
|
||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
|
||||
{
|
||||
/// <summary>A command which moves the player to the given mine level.</summary>
|
||||
[SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
|
||||
internal class SetMineLevelCommand : ConsoleCommand
|
||||
{
|
||||
/*********
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using StardewModdingAPI.Utilities;
|
||||
using StardewValley;
|
||||
|
@ -5,6 +6,7 @@ using StardewValley;
|
|||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
|
||||
{
|
||||
/// <summary>A command which sets the current season.</summary>
|
||||
[SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
|
||||
internal class SetSeasonCommand : ConsoleCommand
|
||||
{
|
||||
/*********
|
||||
|
@ -35,7 +37,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
|
|||
}
|
||||
|
||||
// parse arguments
|
||||
if (!args.TryGet(0, "season", out string season, oneOf: this.ValidSeasons))
|
||||
if (!args.TryGet(0, "season", out string? season, oneOf: this.ValidSeasons))
|
||||
return;
|
||||
|
||||
// handle
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Microsoft.Xna.Framework;
|
||||
using StardewValley;
|
||||
|
@ -5,6 +6,7 @@ using StardewValley;
|
|||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
|
||||
{
|
||||
/// <summary>A command which sets the current time.</summary>
|
||||
[SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
|
||||
internal class SetTimeCommand : ConsoleCommand
|
||||
{
|
||||
/*********
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
using System.Linq;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using StardewModdingAPI.Utilities;
|
||||
using StardewValley;
|
||||
|
||||
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
|
||||
{
|
||||
/// <summary>A command which sets the current year.</summary>
|
||||
[SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")]
|
||||
internal class SetYearCommand : ConsoleCommand
|
||||
{
|
||||
/*********
|
||||
|
|
|
@ -43,16 +43,6 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData
|
|||
this.Item = createItem(this);
|
||||
}
|
||||
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="item">The item metadata to copy.</param>
|
||||
public SearchableItem(SearchableItem item)
|
||||
{
|
||||
this.Type = item.Type;
|
||||
this.ID = item.ID;
|
||||
this.CreateItem = item.CreateItem;
|
||||
this.Item = item.Item;
|
||||
}
|
||||
|
||||
/// <summary>Get whether the item name contains a case-insensitive substring.</summary>
|
||||
/// <param name="substring">The substring to find.</param>
|
||||
public bool NameContains(string substring)
|
||||
|
|
|
@ -31,7 +31,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework
|
|||
/// <param name="itemTypes">The item types to fetch (or null for any type).</param>
|
||||
/// <param name="includeVariants">Whether to include flavored variants like "Sunflower Honey".</param>
|
||||
[SuppressMessage("ReSharper", "AccessToModifiedClosure", Justification = "TryCreate invokes the lambda immediately.")]
|
||||
public IEnumerable<SearchableItem> GetAll(ItemType[] itemTypes = null, bool includeVariants = true)
|
||||
public IEnumerable<SearchableItem> GetAll(ItemType[]? itemTypes = null, bool includeVariants = true)
|
||||
{
|
||||
//
|
||||
//
|
||||
|
@ -43,9 +43,9 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework
|
|||
//
|
||||
//
|
||||
|
||||
IEnumerable<SearchableItem> GetAllRaw()
|
||||
IEnumerable<SearchableItem?> GetAllRaw()
|
||||
{
|
||||
HashSet<ItemType> types = itemTypes?.Any() == true ? new HashSet<ItemType>(itemTypes) : null;
|
||||
HashSet<ItemType>? types = itemTypes?.Any() == true ? new HashSet<ItemType>(itemTypes) : null;
|
||||
bool ShouldGet(ItemType type) => types == null || types.Contains(type);
|
||||
|
||||
// get tools
|
||||
|
@ -106,8 +106,8 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework
|
|||
{
|
||||
foreach (int id in this.TryLoad<int, string>("Data\\weapons").Keys)
|
||||
{
|
||||
yield return this.TryCreate(ItemType.Weapon, id, p => (p.ID >= 32 && p.ID <= 34)
|
||||
? (Item)new Slingshot(p.ID)
|
||||
yield return this.TryCreate(ItemType.Weapon, id, p => p.ID is >= 32 and <= 34
|
||||
? new Slingshot(p.ID)
|
||||
: new MeleeWeapon(p.ID)
|
||||
);
|
||||
}
|
||||
|
@ -132,37 +132,40 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework
|
|||
{
|
||||
foreach (int id in Game1.objectInformation.Keys)
|
||||
{
|
||||
string[] fields = Game1.objectInformation[id]?.Split('/');
|
||||
|
||||
// secret notes
|
||||
if (id == 79)
|
||||
{
|
||||
if (ShouldGet(ItemType.Object))
|
||||
{
|
||||
foreach (int secretNoteId in this.TryLoad<int, string>("Data\\SecretNotes").Keys)
|
||||
{
|
||||
yield return this.TryCreate(ItemType.Object, this.CustomIDOffset + secretNoteId, _ =>
|
||||
{
|
||||
SObject note = new SObject(79, 1);
|
||||
note.name = $"{note.name} #{secretNoteId}";
|
||||
return note;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
string[]? fields = Game1.objectInformation[id]?.Split('/');
|
||||
|
||||
// ring
|
||||
else if (id != 801 && fields?.Length >= 4 && fields[3] == "Ring") // 801 = wedding ring, which isn't an equippable ring
|
||||
if (id != 801 && fields?.Length >= 4 && fields[3] == "Ring") // 801 = wedding ring, which isn't an equippable ring
|
||||
{
|
||||
if (ShouldGet(ItemType.Ring))
|
||||
yield return this.TryCreate(ItemType.Ring, id, p => new Ring(p.ID));
|
||||
}
|
||||
|
||||
// item
|
||||
// journal scrap
|
||||
else if (id == 842)
|
||||
{
|
||||
if (ShouldGet(ItemType.Object))
|
||||
{
|
||||
foreach (SearchableItem? journalScrap in this.GetSecretNotes(isJournalScrap: true))
|
||||
yield return journalScrap;
|
||||
}
|
||||
}
|
||||
|
||||
// secret notes
|
||||
else if (id == 79)
|
||||
{
|
||||
if (ShouldGet(ItemType.Object))
|
||||
{
|
||||
foreach (SearchableItem? secretNote in this.GetSecretNotes(isJournalScrap: false))
|
||||
yield return secretNote;
|
||||
}
|
||||
}
|
||||
|
||||
// object
|
||||
else if (ShouldGet(ItemType.Object))
|
||||
{
|
||||
// spawn main item
|
||||
SObject item = null;
|
||||
SObject? item = null;
|
||||
yield return this.TryCreate(ItemType.Object, id, p =>
|
||||
{
|
||||
return item = (p.ID == 812 // roe
|
||||
|
@ -176,125 +179,179 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework
|
|||
// flavored items
|
||||
if (includeVariants)
|
||||
{
|
||||
switch (item.Category)
|
||||
{
|
||||
// fruit products
|
||||
case SObject.FruitsCategory:
|
||||
// wine
|
||||
yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 2 + item.ParentSheetIndex, _ => new SObject(348, 1)
|
||||
{
|
||||
Name = $"{item.Name} Wine",
|
||||
Price = item.Price * 3,
|
||||
preserve = { SObject.PreserveType.Wine },
|
||||
preservedParentSheetIndex = { item.ParentSheetIndex }
|
||||
});
|
||||
|
||||
// jelly
|
||||
yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 3 + item.ParentSheetIndex, _ => new SObject(344, 1)
|
||||
{
|
||||
Name = $"{item.Name} Jelly",
|
||||
Price = 50 + item.Price * 2,
|
||||
preserve = { SObject.PreserveType.Jelly },
|
||||
preservedParentSheetIndex = { item.ParentSheetIndex }
|
||||
});
|
||||
break;
|
||||
|
||||
// vegetable products
|
||||
case SObject.VegetableCategory:
|
||||
// juice
|
||||
yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 4 + item.ParentSheetIndex, _ => new SObject(350, 1)
|
||||
{
|
||||
Name = $"{item.Name} Juice",
|
||||
Price = (int)(item.Price * 2.25d),
|
||||
preserve = { SObject.PreserveType.Juice },
|
||||
preservedParentSheetIndex = { item.ParentSheetIndex }
|
||||
});
|
||||
|
||||
// pickled
|
||||
yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 5 + item.ParentSheetIndex, _ => new SObject(342, 1)
|
||||
{
|
||||
Name = $"Pickled {item.Name}",
|
||||
Price = 50 + item.Price * 2,
|
||||
preserve = { SObject.PreserveType.Pickle },
|
||||
preservedParentSheetIndex = { item.ParentSheetIndex }
|
||||
});
|
||||
break;
|
||||
|
||||
// flower honey
|
||||
case SObject.flowersCategory:
|
||||
yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 5 + item.ParentSheetIndex, _ =>
|
||||
{
|
||||
SObject honey = new SObject(Vector2.Zero, 340, $"{item.Name} Honey", false, true, false, false)
|
||||
{
|
||||
Name = $"{item.Name} Honey",
|
||||
preservedParentSheetIndex = { item.ParentSheetIndex }
|
||||
};
|
||||
honey.Price += item.Price * 2;
|
||||
return honey;
|
||||
});
|
||||
break;
|
||||
|
||||
// roe and aged roe (derived from FishPond.GetFishProduce)
|
||||
case SObject.sellAtFishShopCategory when item.ParentSheetIndex == 812:
|
||||
{
|
||||
this.GetRoeContextTagLookups(out HashSet<string> simpleTags, out List<List<string>> complexTags);
|
||||
|
||||
foreach (var pair in Game1.objectInformation)
|
||||
{
|
||||
// get input
|
||||
SObject input = this.TryCreate(ItemType.Object, pair.Key, p => new SObject(p.ID, 1))?.Item as SObject;
|
||||
var inputTags = input?.GetContextTags();
|
||||
if (inputTags?.Any() != true)
|
||||
continue;
|
||||
|
||||
// check if roe-producing fish
|
||||
if (!inputTags.Any(tag => simpleTags.Contains(tag)) && !complexTags.Any(set => set.All(tag => input.HasContextTag(tag))))
|
||||
continue;
|
||||
|
||||
// yield roe
|
||||
SObject roe = null;
|
||||
Color color = this.GetRoeColor(input);
|
||||
yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 7 + item.ParentSheetIndex, _ =>
|
||||
{
|
||||
roe = new ColoredObject(812, 1, color)
|
||||
{
|
||||
name = $"{input.Name} Roe",
|
||||
preserve = { Value = SObject.PreserveType.Roe },
|
||||
preservedParentSheetIndex = { Value = input.ParentSheetIndex }
|
||||
};
|
||||
roe.Price += input.Price / 2;
|
||||
return roe;
|
||||
});
|
||||
|
||||
// aged roe
|
||||
if (roe != null && pair.Key != 698) // aged sturgeon roe is caviar, which is a separate item
|
||||
{
|
||||
yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 7 + item.ParentSheetIndex, _ => 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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
foreach (SearchableItem? variant in this.GetFlavoredObjectVariants(item))
|
||||
yield return variant;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return GetAllRaw().Where(p => p != null);
|
||||
return (
|
||||
from item in GetAllRaw()
|
||||
where item != null
|
||||
select item
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/*********
|
||||
** Private methods
|
||||
*********/
|
||||
/// <summary>Get the individual secret note or journal scrap items.</summary>
|
||||
/// <param name="isJournalScrap">Whether to get journal scraps.</param>
|
||||
/// <remarks>Derived from <see cref="GameLocation.tryToCreateUnseenSecretNote"/>.</remarks>
|
||||
private IEnumerable<SearchableItem?> GetSecretNotes(bool isJournalScrap)
|
||||
{
|
||||
// get base item ID
|
||||
int baseId = isJournalScrap ? 842 : 79;
|
||||
|
||||
// get secret note IDs
|
||||
var ids = this
|
||||
.TryLoad<int, string>("Data\\SecretNotes")
|
||||
.Keys
|
||||
.Where(isJournalScrap
|
||||
? id => (id >= GameLocation.JOURNAL_INDEX)
|
||||
: id => (id < GameLocation.JOURNAL_INDEX)
|
||||
)
|
||||
.Select<int, int>(isJournalScrap
|
||||
? id => (id - GameLocation.JOURNAL_INDEX)
|
||||
: id => id
|
||||
);
|
||||
|
||||
// build items
|
||||
foreach (int id in ids)
|
||||
{
|
||||
int fakeId = this.CustomIDOffset * 8 + id;
|
||||
if (isJournalScrap)
|
||||
fakeId += GameLocation.JOURNAL_INDEX;
|
||||
|
||||
yield return this.TryCreate(ItemType.Object, fakeId, _ =>
|
||||
{
|
||||
SObject note = new(baseId, 1);
|
||||
note.Name = $"{note.Name} #{id}";
|
||||
return note;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Get flavored variants of a base item (like Blueberry Wine for Blueberry), if any.</summary>
|
||||
/// <param name="item">A sample of the base item.</param>
|
||||
private IEnumerable<SearchableItem?> GetFlavoredObjectVariants(SObject item)
|
||||
{
|
||||
int id = item.ParentSheetIndex;
|
||||
|
||||
switch (item.Category)
|
||||
{
|
||||
// fruit products
|
||||
case SObject.FruitsCategory:
|
||||
// wine
|
||||
yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 2 + id, _ => new SObject(348, 1)
|
||||
{
|
||||
Name = $"{item.Name} Wine",
|
||||
Price = item.Price * 3,
|
||||
preserve = { SObject.PreserveType.Wine },
|
||||
preservedParentSheetIndex = { id }
|
||||
});
|
||||
|
||||
// jelly
|
||||
yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 3 + id, _ => new SObject(344, 1)
|
||||
{
|
||||
Name = $"{item.Name} Jelly",
|
||||
Price = 50 + item.Price * 2,
|
||||
preserve = { SObject.PreserveType.Jelly },
|
||||
preservedParentSheetIndex = { id }
|
||||
});
|
||||
break;
|
||||
|
||||
// vegetable products
|
||||
case SObject.VegetableCategory:
|
||||
// juice
|
||||
yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 4 + id, _ => new SObject(350, 1)
|
||||
{
|
||||
Name = $"{item.Name} Juice",
|
||||
Price = (int)(item.Price * 2.25d),
|
||||
preserve = { SObject.PreserveType.Juice },
|
||||
preservedParentSheetIndex = { id }
|
||||
});
|
||||
|
||||
// pickled
|
||||
yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 5 + id, _ => new SObject(342, 1)
|
||||
{
|
||||
Name = $"Pickled {item.Name}",
|
||||
Price = 50 + item.Price * 2,
|
||||
preserve = { SObject.PreserveType.Pickle },
|
||||
preservedParentSheetIndex = { id }
|
||||
});
|
||||
break;
|
||||
|
||||
// flower honey
|
||||
case SObject.flowersCategory:
|
||||
yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 5 + id, _ =>
|
||||
{
|
||||
SObject honey = new(Vector2.Zero, 340, $"{item.Name} Honey", false, true, false, false)
|
||||
{
|
||||
Name = $"{item.Name} Honey",
|
||||
preservedParentSheetIndex = { id }
|
||||
};
|
||||
honey.Price += item.Price * 2;
|
||||
return honey;
|
||||
});
|
||||
break;
|
||||
|
||||
// roe and aged roe (derived from FishPond.GetFishProduce)
|
||||
case SObject.sellAtFishShopCategory when id == 812:
|
||||
{
|
||||
this.GetRoeContextTagLookups(out HashSet<string> simpleTags, out List<List<string>> complexTags);
|
||||
|
||||
foreach (var pair in Game1.objectInformation)
|
||||
{
|
||||
// get input
|
||||
SObject? input = this.TryCreate(ItemType.Object, pair.Key, p => new SObject(p.ID, 1))?.Item as SObject;
|
||||
if (input == null)
|
||||
continue;
|
||||
|
||||
HashSet<string> inputTags = input.GetContextTags();
|
||||
if (!inputTags.Any())
|
||||
continue;
|
||||
|
||||
// check if roe-producing fish
|
||||
if (!inputTags.Any(tag => simpleTags.Contains(tag)) && !complexTags.Any(set => set.All(tag => input.HasContextTag(tag))))
|
||||
continue;
|
||||
|
||||
// yield roe
|
||||
SObject? roe = null;
|
||||
Color color = this.GetRoeColor(input);
|
||||
yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 7 + id, _ =>
|
||||
{
|
||||
roe = new ColoredObject(812, 1, color)
|
||||
{
|
||||
name = $"{input.Name} Roe",
|
||||
preserve = { Value = SObject.PreserveType.Roe },
|
||||
preservedParentSheetIndex = { Value = input.ParentSheetIndex }
|
||||
};
|
||||
roe.Price += input.Price / 2;
|
||||
return roe;
|
||||
});
|
||||
|
||||
// aged roe
|
||||
if (roe != null && pair.Key != 698) // aged sturgeon roe is caviar, which is a separate item
|
||||
{
|
||||
yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 7 + id, _ => 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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Get optimized lookups to match items which produce roe in a fish pond.</summary>
|
||||
/// <param name="simpleTags">A lookup of simple singular tags which match a roe-producing fish.</param>
|
||||
/// <param name="complexTags">A list of tag sets which match roe-producing fish.</param>
|
||||
|
@ -320,6 +377,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework
|
|||
/// <typeparam name="TValue">The asset value type.</typeparam>
|
||||
/// <param name="assetName">The data asset name.</param>
|
||||
private Dictionary<TKey, TValue> TryLoad<TKey, TValue>(string assetName)
|
||||
where TKey : notnull
|
||||
{
|
||||
try
|
||||
{
|
||||
|
@ -336,7 +394,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework
|
|||
/// <param name="type">The item type.</param>
|
||||
/// <param name="id">The unique ID (if different from the item's parent sheet index).</param>
|
||||
/// <param name="createItem">Create an item instance.</param>
|
||||
private SearchableItem TryCreate(ItemType type, int id, Func<SearchableItem, Item> createItem)
|
||||
private SearchableItem? TryCreate(ItemType type, int id, Func<SearchableItem, Item> createItem)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
|
|
@ -13,13 +13,13 @@ namespace StardewModdingAPI.Mods.ConsoleCommands
|
|||
** Fields
|
||||
*********/
|
||||
/// <summary>The commands to handle.</summary>
|
||||
private IConsoleCommand[] Commands;
|
||||
private IConsoleCommand[] Commands = null!;
|
||||
|
||||
/// <summary>The commands which may need to handle update ticks.</summary>
|
||||
private IConsoleCommand[] UpdateHandlers;
|
||||
private IConsoleCommand[] UpdateHandlers = null!;
|
||||
|
||||
/// <summary>The commands which may need to handle input.</summary>
|
||||
private IConsoleCommand[] InputHandlers;
|
||||
private IConsoleCommand[] InputHandlers = null!;
|
||||
|
||||
|
||||
/*********
|
||||
|
@ -50,7 +50,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands
|
|||
/// <summary>The method invoked when a button is pressed.</summary>
|
||||
/// <param name="sender">The event sender.</param>
|
||||
/// <param name="e">The event arguments.</param>
|
||||
private void OnButtonPressed(object sender, ButtonPressedEventArgs e)
|
||||
private void OnButtonPressed(object? sender, ButtonPressedEventArgs e)
|
||||
{
|
||||
foreach (IConsoleCommand command in this.InputHandlers)
|
||||
command.OnButtonPressed(this.Monitor, e.Button);
|
||||
|
@ -59,7 +59,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands
|
|||
/// <summary>The method invoked when the game updates its state.</summary>
|
||||
/// <param name="sender">The event sender.</param>
|
||||
/// <param name="e">The event arguments.</param>
|
||||
private void OnUpdateTicked(object sender, EventArgs e)
|
||||
private void OnUpdateTicked(object? sender, EventArgs e)
|
||||
{
|
||||
foreach (IConsoleCommand command in this.UpdateHandlers)
|
||||
command.OnUpdated(this.Monitor);
|
||||
|
@ -71,7 +71,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands
|
|||
/// <param name="args">The command arguments.</param>
|
||||
private void HandleCommand(IConsoleCommand command, string commandName, string[] args)
|
||||
{
|
||||
ArgumentParser argParser = new ArgumentParser(commandName, args, this.Monitor);
|
||||
ArgumentParser argParser = new(commandName, args, this.Monitor);
|
||||
command.Handle(this.Monitor, commandName, argParser);
|
||||
}
|
||||
|
||||
|
@ -81,7 +81,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands
|
|||
return (
|
||||
from type in this.GetType().Assembly.GetTypes()
|
||||
where !type.IsAbstract && typeof(IConsoleCommand).IsAssignableFrom(type)
|
||||
select (IConsoleCommand)Activator.CreateInstance(type)
|
||||
select (IConsoleCommand)Activator.CreateInstance(type)!
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"Name": "Console Commands",
|
||||
"Author": "SMAPI",
|
||||
"Version": "3.13.4",
|
||||
"Version": "3.14.0",
|
||||
"Description": "Adds SMAPI console commands that let you manipulate the game.",
|
||||
"UniqueID": "SMAPI.ConsoleCommands",
|
||||
"EntryDll": "ConsoleCommands.dll",
|
||||
"MinimumApiVersion": "3.13.4"
|
||||
"MinimumApiVersion": "3.14.0"
|
||||
}
|
||||
|
|
|
@ -30,7 +30,6 @@ namespace StardewModdingAPI.Mods.ErrorHandler
|
|||
// apply patches
|
||||
HarmonyPatcher.Apply(this.ModManifest.UniqueID, this.Monitor,
|
||||
new DialoguePatcher(monitorForGame, this.Helper.Reflection),
|
||||
new DictionaryPatcher(this.Helper.Reflection),
|
||||
new EventPatcher(monitorForGame),
|
||||
new GameLocationPatcher(monitorForGame),
|
||||
new IClickableMenuPatcher(),
|
||||
|
@ -58,7 +57,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler
|
|||
/// <summary>The method invoked when a save is loaded.</summary>
|
||||
/// <param name="sender">The event sender.</param>
|
||||
/// <param name="e">The event arguments.</param>
|
||||
private void OnSaveLoaded(object sender, SaveLoadedEventArgs e)
|
||||
private void OnSaveLoaded(object? sender, SaveLoadedEventArgs e)
|
||||
{
|
||||
// show in-game warning for removed save content
|
||||
if (this.IsSaveContentRemoved)
|
||||
|
@ -81,7 +80,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler
|
|||
MethodInfo getMonitorForGame = coreType.GetMethod("GetMonitorForGame")
|
||||
?? throw new InvalidOperationException("Can't access the SMAPI's 'GetMonitorForGame' method. This mod may not work correctly.");
|
||||
|
||||
return (IMonitor)getMonitorForGame.Invoke(core, new object[0]) ?? this.Monitor;
|
||||
return (IMonitor?)getMonitorForGame.Invoke(core, Array.Empty<object>()) ?? this.Monitor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,10 +17,10 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches
|
|||
** Fields
|
||||
*********/
|
||||
/// <summary>Writes messages to the console and log file on behalf of the game.</summary>
|
||||
private static IMonitor MonitorForGame;
|
||||
private static IMonitor MonitorForGame = null!;
|
||||
|
||||
/// <summary>Simplifies access to private code.</summary>
|
||||
private static IReflectionHelper Reflection;
|
||||
private static IReflectionHelper Reflection = null!;
|
||||
|
||||
|
||||
/*********
|
||||
|
@ -54,12 +54,12 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches
|
|||
/// <param name="speaker">The NPC for which the dialogue is being parsed.</param>
|
||||
/// <param name="__exception">The exception thrown by the wrapped method, if any.</param>
|
||||
/// <returns>Returns the exception to throw, if any.</returns>
|
||||
private static Exception Finalize_Constructor(Dialogue __instance, string masterDialogue, NPC speaker, Exception __exception)
|
||||
private static Exception? Finalize_Constructor(Dialogue __instance, string masterDialogue, NPC? speaker, Exception? __exception)
|
||||
{
|
||||
if (__exception != null)
|
||||
{
|
||||
// log message
|
||||
string name = !string.IsNullOrWhiteSpace(speaker?.Name) ? speaker.Name : null;
|
||||
string? name = !string.IsNullOrWhiteSpace(speaker?.Name) ? speaker.Name : null;
|
||||
DialoguePatcher.MonitorForGame.Log($"Failed parsing dialogue string{(name != null ? $" for {name}" : "")}:\n{masterDialogue}\n{__exception.GetLogSummary()}", LogLevel.Error);
|
||||
|
||||
// set default dialogue
|
||||
|
|
|
@ -1,98 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using HarmonyLib;
|
||||
using StardewModdingAPI.Internal.Patching;
|
||||
using StardewValley.GameData;
|
||||
using StardewValley.GameData.HomeRenovations;
|
||||
using StardewValley.GameData.Movies;
|
||||
|
||||
namespace StardewModdingAPI.Mods.ErrorHandler.Patches
|
||||
{
|
||||
/// <summary>Harmony patches for <see cref="Dictionary{TKey,TValue}"/> which add the accessed key to <see cref="KeyNotFoundException"/> exceptions.</summary>
|
||||
/// <remarks>Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments.</remarks>
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")]
|
||||
[SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")]
|
||||
internal class DictionaryPatcher : BasePatcher
|
||||
{
|
||||
/*********
|
||||
** Fields
|
||||
*********/
|
||||
/// <summary>Simplifies access to private code.</summary>
|
||||
private static IReflectionHelper Reflection;
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="reflector">Simplifies access to private code.</param>
|
||||
public DictionaryPatcher(IReflectionHelper reflector)
|
||||
{
|
||||
DictionaryPatcher.Reflection = reflector;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Apply(Harmony harmony, IMonitor monitor)
|
||||
{
|
||||
Type[] keyTypes = { typeof(int), typeof(string) };
|
||||
Type[] valueTypes = { typeof(int), typeof(string), typeof(HomeRenovation), typeof(MovieData), typeof(SpecialOrderData) };
|
||||
|
||||
foreach (Type keyType in keyTypes)
|
||||
{
|
||||
foreach (Type valueType in valueTypes)
|
||||
{
|
||||
Type dictionaryType = typeof(Dictionary<,>).MakeGenericType(keyType, valueType);
|
||||
|
||||
harmony.Patch(
|
||||
original: AccessTools.Method(dictionaryType, "get_Item") ?? throw new InvalidOperationException($"Can't find method {PatchHelper.GetMethodString(dictionaryType, "get_Item")} to patch."),
|
||||
finalizer: this.GetHarmonyMethod(nameof(DictionaryPatcher.Finalize_GetItem))
|
||||
);
|
||||
|
||||
harmony.Patch(
|
||||
original: AccessTools.Method(dictionaryType, "Add") ?? throw new InvalidOperationException($"Can't find method {PatchHelper.GetMethodString(dictionaryType, "Add")} to patch."),
|
||||
finalizer: this.GetHarmonyMethod(nameof(DictionaryPatcher.Finalize_Add))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*********
|
||||
** Private methods
|
||||
*********/
|
||||
/// <summary>The method to call after the dictionary indexer throws an exception.</summary>
|
||||
/// <param name="key">The dictionary key being fetched.</param>
|
||||
/// <param name="__exception">The exception thrown by the wrapped method, if any.</param>
|
||||
/// <returns>Returns the exception to throw, if any.</returns>
|
||||
private static Exception Finalize_GetItem(object key, Exception __exception)
|
||||
{
|
||||
if (__exception is KeyNotFoundException)
|
||||
DictionaryPatcher.AddKey(__exception, key);
|
||||
|
||||
return __exception;
|
||||
}
|
||||
|
||||
/// <summary>The method to call after a dictionary insert throws an exception.</summary>
|
||||
/// <param name="key">The dictionary key being inserted.</param>
|
||||
/// <param name="__exception">The exception thrown by the wrapped method, if any.</param>
|
||||
/// <returns>Returns the exception to throw, if any.</returns>
|
||||
private static Exception Finalize_Add(object key, Exception __exception)
|
||||
{
|
||||
if (__exception is ArgumentException)
|
||||
DictionaryPatcher.AddKey(__exception, key);
|
||||
|
||||
return __exception;
|
||||
}
|
||||
|
||||
/// <summary>Add the dictionary key to an exception message.</summary>
|
||||
/// <param name="exception">The exception whose message to edit.</param>
|
||||
/// <param name="key">The dictionary key.</param>
|
||||
private static void AddKey(Exception exception, object key)
|
||||
{
|
||||
DictionaryPatcher.Reflection
|
||||
.GetField<string>(exception, "_message")
|
||||
.SetValue($"{exception.Message}\nkey: '{key}'");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,7 +16,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches
|
|||
** Fields
|
||||
*********/
|
||||
/// <summary>Writes messages to the console and log file on behalf of the game.</summary>
|
||||
private static IMonitor MonitorForGame;
|
||||
private static IMonitor MonitorForGame = null!;
|
||||
|
||||
|
||||
/*********
|
||||
|
|
|
@ -17,8 +17,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches
|
|||
** Fields
|
||||
*********/
|
||||
/// <summary>Writes messages to the console and log file on behalf of the game.</summary>
|
||||
private static IMonitor MonitorForGame;
|
||||
|
||||
private static IMonitor MonitorForGame = null!;
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
|
@ -52,7 +51,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches
|
|||
/// <param name="precondition">The precondition to be parsed.</param>
|
||||
/// <param name="__exception">The exception thrown by the wrapped method, if any.</param>
|
||||
/// <returns>Returns the exception to throw, if any.</returns>
|
||||
private static Exception Finalize_CheckEventPrecondition(ref int __result, string precondition, Exception __exception)
|
||||
private static Exception? Finalize_CheckEventPrecondition(ref int __result, string precondition, Exception? __exception)
|
||||
{
|
||||
if (__exception != null)
|
||||
{
|
||||
|
@ -68,7 +67,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches
|
|||
/// <param name="map">The map whose tilesheets to update.</param>
|
||||
/// <param name="__exception">The exception thrown by the wrapped method, if any.</param>
|
||||
/// <returns>Returns the exception to throw, if any.</returns>
|
||||
private static Exception Finalize_UpdateSeasonalTileSheets(GameLocation __instance, Map map, Exception __exception)
|
||||
private static Exception? Finalize_UpdateSeasonalTileSheets(GameLocation __instance, Map map, Exception? __exception)
|
||||
{
|
||||
if (__exception != null)
|
||||
GameLocationPatcher.MonitorForGame.Log($"Failed updating seasonal tilesheets for location '{__instance.NameOrUniqueName}': \n{__exception}", LogLevel.Error);
|
||||
|
|
|
@ -18,7 +18,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches
|
|||
** Fields
|
||||
*********/
|
||||
/// <summary>Writes messages to the console and log file on behalf of the game.</summary>
|
||||
private static IMonitor MonitorForGame;
|
||||
private static IMonitor MonitorForGame = null!;
|
||||
|
||||
|
||||
/*********
|
||||
|
@ -54,7 +54,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches
|
|||
/// <param name="__result">The return value of the original method.</param>
|
||||
/// <param name="__exception">The exception thrown by the wrapped method, if any.</param>
|
||||
/// <returns>Returns the exception to throw, if any.</returns>
|
||||
private static Exception Finalize_CurrentDialogue(NPC __instance, ref Stack<Dialogue> __result, Exception __exception)
|
||||
private static Exception? Finalize_CurrentDialogue(NPC __instance, ref Stack<Dialogue> __result, Exception? __exception)
|
||||
{
|
||||
if (__exception == null)
|
||||
return null;
|
||||
|
@ -71,7 +71,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches
|
|||
/// <param name="__result">The patched method's return value.</param>
|
||||
/// <param name="__exception">The exception thrown by the wrapped method, if any.</param>
|
||||
/// <returns>Returns the exception to throw, if any.</returns>
|
||||
private static Exception Finalize_ParseMasterSchedule(string rawData, NPC __instance, ref Dictionary<int, SchedulePathDescription> __result, Exception __exception)
|
||||
private static Exception? Finalize_ParseMasterSchedule(string rawData, NPC __instance, ref Dictionary<int, SchedulePathDescription> __result, Exception? __exception)
|
||||
{
|
||||
if (__exception != null)
|
||||
{
|
||||
|
|
|
@ -57,7 +57,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches
|
|||
/// <param name="__result">The patched method's return value.</param>
|
||||
/// <param name="__exception">The exception thrown by the wrapped method, if any.</param>
|
||||
/// <returns>Returns the exception to throw, if any.</returns>
|
||||
private static Exception Finalize_Object_loadDisplayName(ref string __result, Exception __exception)
|
||||
private static Exception? Finalize_Object_loadDisplayName(ref string __result, Exception? __exception)
|
||||
{
|
||||
if (__exception is KeyNotFoundException)
|
||||
{
|
||||
|
|
|
@ -22,10 +22,10 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches
|
|||
** Fields
|
||||
*********/
|
||||
/// <summary>Writes messages to the console and log file.</summary>
|
||||
private static IMonitor Monitor;
|
||||
private static IMonitor Monitor = null!;
|
||||
|
||||
/// <summary>A callback invoked when custom content is removed from the save data to avoid a crash.</summary>
|
||||
private static Action OnContentRemoved;
|
||||
private static Action OnContentRemoved = null!;
|
||||
|
||||
|
||||
/*********
|
||||
|
@ -74,10 +74,10 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches
|
|||
/// <summary>The method to call after <see cref="SaveGame.LoadFarmType"/> throws an exception.</summary>
|
||||
/// <param name="__exception">The exception thrown by the wrapped method, if any.</param>
|
||||
/// <returns>Returns the exception to throw, if any.</returns>
|
||||
private static Exception Finalize_LoadFarmType(Exception __exception)
|
||||
private static Exception? Finalize_LoadFarmType(Exception? __exception)
|
||||
{
|
||||
// missing custom farm type
|
||||
if (__exception?.Message?.Contains("not a valid farm type") == true && !int.TryParse(SaveGame.loaded.whichFarm, out _))
|
||||
if (__exception?.Message.Contains("not a valid farm type") == true && !int.TryParse(SaveGame.loaded.whichFarm, out _))
|
||||
{
|
||||
SaveGamePatcher.Monitor.Log(__exception.GetLogSummary(), LogLevel.Error);
|
||||
SaveGamePatcher.Monitor.Log($"Removed invalid custom farm type '{SaveGame.loaded.whichFarm}' to avoid a crash when loading save '{Constants.SaveFolderName}'. (Did you remove a custom farm type mod?)", LogLevel.Warn);
|
||||
|
@ -108,7 +108,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches
|
|||
/// <summary>Remove content which no longer exists in the game data.</summary>
|
||||
/// <param name="location">The current game location.</param>
|
||||
/// <param name="npcs">The NPC data.</param>
|
||||
private static bool RemoveBrokenContent(GameLocation location, IDictionary<string, string> npcs)
|
||||
private static bool RemoveBrokenContent(GameLocation? location, IDictionary<string, string> npcs)
|
||||
{
|
||||
bool removedAny = false;
|
||||
if (location == null)
|
||||
|
@ -121,7 +121,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches
|
|||
{
|
||||
try
|
||||
{
|
||||
BluePrint _ = new BluePrint(building.buildingType.Value);
|
||||
BluePrint _ = new(building.buildingType.Value);
|
||||
}
|
||||
catch (ContentLoadException)
|
||||
{
|
||||
|
|
|
@ -28,9 +28,9 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches
|
|||
/*********
|
||||
** Private methods
|
||||
*********/
|
||||
/// <summary>The method to call after <see cref="SpriteBatch.CheckValid"/>.</summary>
|
||||
/// <summary>The method to call after <see cref="SpriteBatch.CheckValid(Texture2D)"/>.</summary>
|
||||
/// <param name="texture">The texture to validate.</param>
|
||||
private static void After_CheckValid(Texture2D texture)
|
||||
private static void After_CheckValid(Texture2D? texture)
|
||||
{
|
||||
if (texture?.IsDisposed == true)
|
||||
throw new ObjectDisposedException("Cannot draw this texture because it's disposed.");
|
||||
|
|
|
@ -33,7 +33,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches
|
|||
/// <param name="delimiter">The delimiter by which to split the text description.</param>
|
||||
/// <param name="__exception">The exception thrown by the wrapped method, if any.</param>
|
||||
/// <returns>Returns the exception to throw, if any.</returns>
|
||||
private static Exception Finalize_GetItemFromStandardTextDescription(string description, char delimiter, ref Exception __exception)
|
||||
private static Exception? Finalize_GetItemFromStandardTextDescription(string description, char delimiter, ref Exception? __exception)
|
||||
{
|
||||
return __exception != null
|
||||
? new FormatException($"Failed to parse item text description \"{description}\" with delimiter \"{delimiter}\".", __exception)
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"Name": "Error Handler",
|
||||
"Author": "SMAPI",
|
||||
"Version": "3.13.4",
|
||||
"Version": "3.14.0",
|
||||
"Description": "Handles some common vanilla errors to log more useful info or avoid breaking the game.",
|
||||
"UniqueID": "SMAPI.ErrorHandler",
|
||||
"EntryDll": "ErrorHandler.dll",
|
||||
"MinimumApiVersion": "3.13.4"
|
||||
"MinimumApiVersion": "3.14.0"
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
|
@ -19,7 +20,7 @@ namespace StardewModdingAPI.Mods.SaveBackup
|
|||
private readonly int BackupsToKeep = 10;
|
||||
|
||||
/// <summary>The absolute path to the folder in which to store save backups.</summary>
|
||||
private readonly string BackupFolder = Path.Combine(Constants.ExecutionPath, "save-backups");
|
||||
private readonly string BackupFolder = Path.Combine(Constants.GamePath, "save-backups");
|
||||
|
||||
/// <summary>A unique label for the save backup to create.</summary>
|
||||
private readonly string BackupLabel = $"{DateTime.UtcNow:yyyy-MM-dd} - SMAPI {Constants.ApiVersion} with Stardew Valley {Game1.version}";
|
||||
|
@ -38,13 +39,13 @@ namespace StardewModdingAPI.Mods.SaveBackup
|
|||
try
|
||||
{
|
||||
// init backup folder
|
||||
DirectoryInfo backupFolder = new DirectoryInfo(this.BackupFolder);
|
||||
DirectoryInfo backupFolder = new(this.BackupFolder);
|
||||
backupFolder.Create();
|
||||
|
||||
// back up & prune saves
|
||||
Task
|
||||
.Run(() => this.CreateBackup(backupFolder))
|
||||
.ContinueWith(backupTask => this.PruneBackups(backupFolder, this.BackupsToKeep));
|
||||
.ContinueWith(_ => this.PruneBackups(backupFolder, this.BackupsToKeep));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -63,8 +64,8 @@ namespace StardewModdingAPI.Mods.SaveBackup
|
|||
try
|
||||
{
|
||||
// get target path
|
||||
FileInfo targetFile = new FileInfo(Path.Combine(backupFolder.FullName, this.FileName));
|
||||
DirectoryInfo fallbackDir = new DirectoryInfo(Path.Combine(backupFolder.FullName, this.BackupLabel));
|
||||
FileInfo targetFile = new(Path.Combine(backupFolder.FullName, this.FileName));
|
||||
DirectoryInfo fallbackDir = new(Path.Combine(backupFolder.FullName, this.BackupLabel));
|
||||
if (targetFile.Exists || fallbackDir.Exists)
|
||||
{
|
||||
this.Monitor.Log("Already backed up today.");
|
||||
|
@ -72,7 +73,7 @@ namespace StardewModdingAPI.Mods.SaveBackup
|
|||
}
|
||||
|
||||
// copy saves to fallback directory (ignore non-save files/folders)
|
||||
DirectoryInfo savesDir = new DirectoryInfo(Constants.SavesPath);
|
||||
DirectoryInfo savesDir = new(Constants.SavesPath);
|
||||
if (!this.RecursiveCopy(savesDir, fallbackDir, entry => this.MatchSaveFolders(savesDir, entry), copyRoot: false))
|
||||
{
|
||||
this.Monitor.Log("No saves found.");
|
||||
|
@ -80,7 +81,7 @@ namespace StardewModdingAPI.Mods.SaveBackup
|
|||
}
|
||||
|
||||
// compress backup if possible
|
||||
if (!this.TryCompress(fallbackDir.FullName, targetFile, out Exception compressError))
|
||||
if (!this.TryCompress(fallbackDir.FullName, targetFile, out Exception? compressError))
|
||||
{
|
||||
this.Monitor.Log(Constants.TargetPlatform != GamePlatform.Android
|
||||
? $"Backed up to {fallbackDir.FullName}." // expected to fail on Android
|
||||
|
@ -140,7 +141,7 @@ namespace StardewModdingAPI.Mods.SaveBackup
|
|||
/// <param name="destination">The destination file to create.</param>
|
||||
/// <param name="error">The error which occurred trying to compress, if applicable. This is <see cref="NotSupportedException"/> if compression isn't supported on this platform.</param>
|
||||
/// <returns>Returns whether compression succeeded.</returns>
|
||||
private bool TryCompress(string sourcePath, FileInfo destination, out Exception error)
|
||||
private bool TryCompress(string sourcePath, FileInfo destination, [NotNullWhen(false)] out Exception? error)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
@ -170,8 +171,8 @@ namespace StardewModdingAPI.Mods.SaveBackup
|
|||
try
|
||||
{
|
||||
// create compressed backup
|
||||
Assembly coreAssembly = Assembly.Load("System.IO.Compression, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") ?? throw new InvalidOperationException("Can't load System.IO.Compression assembly.");
|
||||
Assembly fsAssembly = Assembly.Load("System.IO.Compression.FileSystem, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089") ?? throw new InvalidOperationException("Can't load System.IO.Compression assembly.");
|
||||
Assembly coreAssembly = Assembly.Load("System.IO.Compression, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089");
|
||||
Assembly fsAssembly = Assembly.Load("System.IO.Compression.FileSystem, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089");
|
||||
Type compressionLevelType = coreAssembly.GetType("System.IO.Compression.CompressionLevel") ?? throw new InvalidOperationException("Can't load CompressionLevel type.");
|
||||
Type zipFileType = fsAssembly.GetType("System.IO.Compression.ZipFile") ?? throw new InvalidOperationException("Can't load ZipFile type.");
|
||||
createFromDirectory = zipFileType.GetMethod("CreateFromDirectory", new[] { typeof(string), typeof(string), compressionLevelType, typeof(bool) }) ?? throw new InvalidOperationException("Can't load ZipFile.CreateFromDirectory method.");
|
||||
|
@ -190,8 +191,8 @@ namespace StardewModdingAPI.Mods.SaveBackup
|
|||
/// <param name="destination">The destination file to create.</param>
|
||||
private void CompressUsingMacProcess(string sourcePath, FileInfo destination)
|
||||
{
|
||||
DirectoryInfo saveFolder = new DirectoryInfo(sourcePath);
|
||||
ProcessStartInfo startInfo = new ProcessStartInfo
|
||||
DirectoryInfo saveFolder = new(sourcePath);
|
||||
ProcessStartInfo startInfo = new()
|
||||
{
|
||||
FileName = "zip",
|
||||
Arguments = $"-rq \"{destination.FullName}\" \"{saveFolder.Name}\" -x \"*.DS_Store\" -x \"__MACOSX\"",
|
||||
|
@ -208,7 +209,7 @@ namespace StardewModdingAPI.Mods.SaveBackup
|
|||
/// <param name="filter">A filter which matches the files or directories to copy, or <c>null</c> to copy everything.</param>
|
||||
/// <remarks>Derived from the SMAPI installer code.</remarks>
|
||||
/// <returns>Returns whether any files were copied.</returns>
|
||||
private bool RecursiveCopy(FileSystemInfo source, DirectoryInfo targetFolder, Func<FileSystemInfo, bool> filter, bool copyRoot = true)
|
||||
private bool RecursiveCopy(FileSystemInfo source, DirectoryInfo targetFolder, Func<FileSystemInfo, bool>? filter, bool copyRoot = true)
|
||||
{
|
||||
if (!source.Exists || filter?.Invoke(source) == false)
|
||||
return false;
|
||||
|
@ -242,7 +243,7 @@ namespace StardewModdingAPI.Mods.SaveBackup
|
|||
private bool MatchSaveFolders(DirectoryInfo savesFolder, FileSystemInfo entry)
|
||||
{
|
||||
// only need to filter top-level entries
|
||||
string parentPath = (entry as FileInfo)?.DirectoryName ?? (entry as DirectoryInfo)?.Parent?.FullName;
|
||||
string? parentPath = (entry as FileInfo)?.DirectoryName ?? (entry as DirectoryInfo)?.Parent?.FullName;
|
||||
if (parentPath != savesFolder.FullName)
|
||||
return true;
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"Name": "Save Backup",
|
||||
"Author": "SMAPI",
|
||||
"Version": "3.13.4",
|
||||
"Version": "3.14.0",
|
||||
"Description": "Automatically backs up all your saves once per day into its folder.",
|
||||
"UniqueID": "SMAPI.SaveBackup",
|
||||
"EntryDll": "SaveBackup.dll",
|
||||
"MinimumApiVersion": "3.13.4"
|
||||
"MinimumApiVersion": "3.14.0"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
using System;
|
||||
using SMAPI.Tests.ModApiConsumer.Interfaces;
|
||||
|
||||
namespace SMAPI.Tests.ModApiConsumer
|
||||
{
|
||||
/// <summary>A simulated API consumer.</summary>
|
||||
public class ApiConsumer
|
||||
{
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Call the event field on the given API.</summary>
|
||||
/// <param name="api">The API to call.</param>
|
||||
/// <param name="getValues">Get the number of times the event was called and the last value received.</param>
|
||||
public void UseEventField(ISimpleApi api, out Func<(int timesCalled, int actualValue)> getValues)
|
||||
{
|
||||
// act
|
||||
int calls = 0;
|
||||
int lastValue = -1;
|
||||
api.OnEventRaised += (_, value) =>
|
||||
{
|
||||
calls++;
|
||||
lastValue = value;
|
||||
};
|
||||
|
||||
getValues = () => (timesCalled: calls, actualValue: lastValue);
|
||||
}
|
||||
|
||||
/// <summary>Call the event property on the given API.</summary>
|
||||
/// <param name="api">The API to call.</param>
|
||||
/// <param name="getValues">Get the number of times the event was called and the last value received.</param>
|
||||
public void UseEventProperty(ISimpleApi api, out Func<(int timesCalled, int actualValue)> getValues)
|
||||
{
|
||||
// act
|
||||
int calls = 0;
|
||||
int lastValue = -1;
|
||||
api.OnEventRaisedProperty += (_, value) =>
|
||||
{
|
||||
calls++;
|
||||
lastValue = value;
|
||||
};
|
||||
|
||||
getValues = () => (timesCalled: calls, actualValue: lastValue);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using StardewModdingAPI.Utilities;
|
||||
|
||||
namespace SMAPI.Tests.ModApiConsumer.Interfaces
|
||||
{
|
||||
/// <summary>A mod-provided API which provides basic events, properties, and methods.</summary>
|
||||
public interface ISimpleApi
|
||||
{
|
||||
/*********
|
||||
** Test interface
|
||||
*********/
|
||||
/****
|
||||
** Events
|
||||
****/
|
||||
/// <summary>A simple event field.</summary>
|
||||
event EventHandler<int> OnEventRaised;
|
||||
|
||||
/// <summary>A simple event property with custom add/remove logic.</summary>
|
||||
event EventHandler<int> OnEventRaisedProperty;
|
||||
|
||||
|
||||
/****
|
||||
** Properties
|
||||
****/
|
||||
/// <summary>A simple numeric property.</summary>
|
||||
int NumberProperty { get; set; }
|
||||
|
||||
/// <summary>A simple object property.</summary>
|
||||
object ObjectProperty { get; set; }
|
||||
|
||||
/// <summary>A simple list property.</summary>
|
||||
List<string> ListProperty { get; set; }
|
||||
|
||||
/// <summary>A simple list property with an interface.</summary>
|
||||
IList<string> ListPropertyWithInterface { get; set; }
|
||||
|
||||
/// <summary>A property with nested generics.</summary>
|
||||
IDictionary<string, IList<string>> GenericsProperty { get; set; }
|
||||
|
||||
/// <summary>A property using an enum available to both mods.</summary>
|
||||
BindingFlags EnumProperty { get; set; }
|
||||
|
||||
/// <summary>A read-only property.</summary>
|
||||
int GetterProperty { get; }
|
||||
|
||||
|
||||
/****
|
||||
** Methods
|
||||
****/
|
||||
/// <summary>A simple method with no return value.</summary>
|
||||
void GetNothing();
|
||||
|
||||
/// <summary>A simple method which returns a number.</summary>
|
||||
int GetInt(int value);
|
||||
|
||||
/// <summary>A simple method which returns an object.</summary>
|
||||
object GetObject(object value);
|
||||
|
||||
/// <summary>A simple method which returns a list.</summary>
|
||||
List<string> GetList(string value);
|
||||
|
||||
/// <summary>A simple method which returns a list with an interface.</summary>
|
||||
IList<string> GetListWithInterface(string value);
|
||||
|
||||
/// <summary>A simple method which returns nested generics.</summary>
|
||||
IDictionary<string, IList<string>> GetGenerics(string key, string value);
|
||||
|
||||
/// <summary>A simple method which returns a lambda.</summary>
|
||||
Func<string, string> GetLambda(Func<string, string> value);
|
||||
|
||||
/// <summary>A simple method which returns out parameters.</summary>
|
||||
bool TryGetOutParameter(int inputNumber, out int outNumber, out string outString, out PerScreen<int> outReference, out IDictionary<int, PerScreen<int>> outComplexType);
|
||||
|
||||
|
||||
/****
|
||||
** Inherited members
|
||||
****/
|
||||
/// <summary>A property inherited from a base class.</summary>
|
||||
public string InheritedProperty { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
This project contains a simulated [mod-provided API] consumer used in the API proxying unit tests.
|
||||
|
||||
[mod-provided API]: https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Integrations
|
|
@ -0,0 +1,11 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="..\..\build\common.targets" />
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SMAPI\SMAPI.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
|
@ -0,0 +1,12 @@
|
|||
namespace SMAPI.Tests.ModApiProvider.Framework
|
||||
{
|
||||
/// <summary>The base class for <see cref="SimpleApi"/>.</summary>
|
||||
public class BaseApi
|
||||
{
|
||||
/*********
|
||||
** Test interface
|
||||
*********/
|
||||
/// <summary>A property inherited from a base class.</summary>
|
||||
public string? InheritedProperty { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
// ReSharper disable UnusedMember.Global -- used dynamically through proxies
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using StardewModdingAPI.Utilities;
|
||||
|
||||
namespace SMAPI.Tests.ModApiProvider.Framework
|
||||
{
|
||||
/// <summary>A mod-provided API which provides basic events, properties, and methods.</summary>
|
||||
public class SimpleApi : BaseApi
|
||||
{
|
||||
/*********
|
||||
** Test interface
|
||||
*********/
|
||||
/****
|
||||
** Events
|
||||
****/
|
||||
/// <summary>A simple event field.</summary>
|
||||
public event EventHandler<int>? OnEventRaised;
|
||||
|
||||
/// <summary>A simple event property with custom add/remove logic.</summary>
|
||||
public event EventHandler<int> OnEventRaisedProperty
|
||||
{
|
||||
add => this.OnEventRaised += value;
|
||||
remove => this.OnEventRaised -= value;
|
||||
}
|
||||
|
||||
|
||||
/****
|
||||
** Properties
|
||||
****/
|
||||
/// <summary>A simple numeric property.</summary>
|
||||
public int NumberProperty { get; set; }
|
||||
|
||||
/// <summary>A simple object property.</summary>
|
||||
public object? ObjectProperty { get; set; }
|
||||
|
||||
/// <summary>A simple list property.</summary>
|
||||
public List<string>? ListProperty { get; set; }
|
||||
|
||||
/// <summary>A simple list property with an interface.</summary>
|
||||
public IList<string>? ListPropertyWithInterface { get; set; }
|
||||
|
||||
/// <summary>A property with nested generics.</summary>
|
||||
public IDictionary<string, IList<string>>? GenericsProperty { get; set; }
|
||||
|
||||
/// <summary>A property using an enum available to both mods.</summary>
|
||||
public BindingFlags EnumProperty { get; set; }
|
||||
|
||||
/// <summary>A read-only property.</summary>
|
||||
public int GetterProperty => 42;
|
||||
|
||||
|
||||
/****
|
||||
** Methods
|
||||
****/
|
||||
/// <summary>A simple method with no return value.</summary>
|
||||
public void GetNothing() { }
|
||||
|
||||
/// <summary>A simple method which returns a number.</summary>
|
||||
public int GetInt(int value)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>A simple method which returns an object.</summary>
|
||||
public object GetObject(object value)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>A simple method which returns a list.</summary>
|
||||
public List<string> GetList(string value)
|
||||
{
|
||||
return new() { value };
|
||||
}
|
||||
|
||||
/// <summary>A simple method which returns a list with an interface.</summary>
|
||||
public IList<string> GetListWithInterface(string value)
|
||||
{
|
||||
return new List<string> { value };
|
||||
}
|
||||
|
||||
/// <summary>A simple method which returns nested generics.</summary>
|
||||
public IDictionary<string, IList<string>> GetGenerics(string key, string value)
|
||||
{
|
||||
return new Dictionary<string, IList<string>>
|
||||
{
|
||||
[key] = new List<string> { value }
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>A simple method which returns a lambda.</summary>
|
||||
public Func<string, string> GetLambda(Func<string, string> value)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>A simple method which returns out parameters.</summary>
|
||||
public bool TryGetOutParameter(int inputNumber, out int outNumber, out string outString, out PerScreen<int> outReference, out IDictionary<int, PerScreen<int>> outComplexType)
|
||||
{
|
||||
outNumber = inputNumber;
|
||||
outString = inputNumber.ToString();
|
||||
outReference = new PerScreen<int>(() => inputNumber);
|
||||
outComplexType = new Dictionary<int, PerScreen<int>>
|
||||
{
|
||||
[inputNumber] = new PerScreen<int>(() => inputNumber)
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/*********
|
||||
** Helper methods
|
||||
*********/
|
||||
/// <summary>Raise the <see cref="OnEventRaised"/> event.</summary>
|
||||
/// <param name="value">The value to pass to the event.</param>
|
||||
public void RaiseEventField(int value)
|
||||
{
|
||||
this.OnEventRaised?.Invoke(null, value);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using SMAPI.Tests.ModApiProvider.Framework;
|
||||
|
||||
namespace SMAPI.Tests.ModApiProvider
|
||||
{
|
||||
/// <summary>A simulated mod instance.</summary>
|
||||
public class ProviderMod
|
||||
{
|
||||
/// <summary>The underlying API instance.</summary>
|
||||
private readonly SimpleApi Api = new();
|
||||
|
||||
/// <summary>Get the mod API instance.</summary>
|
||||
public object GetModApi()
|
||||
{
|
||||
return this.Api;
|
||||
}
|
||||
|
||||
/// <summary>Raise the <see cref="SimpleApi.OnEventRaised"/> event.</summary>
|
||||
/// <param name="value">The value to send as an event argument.</param>
|
||||
public void RaiseEvent(int value)
|
||||
{
|
||||
this.Api.RaiseEventField(value);
|
||||
}
|
||||
|
||||
/// <summary>Set the values for the API property.</summary>
|
||||
public void SetPropertyValues(int number, object obj, string listValue, string listWithInterfaceValue, string dictionaryKey, string dictionaryListValue, BindingFlags enumValue, string inheritedValue)
|
||||
{
|
||||
this.Api.NumberProperty = number;
|
||||
this.Api.ObjectProperty = obj;
|
||||
this.Api.ListProperty = new List<string> { listValue };
|
||||
this.Api.ListPropertyWithInterface = new List<string> { listWithInterfaceValue };
|
||||
this.Api.GenericsProperty = new Dictionary<string, IList<string>> { [dictionaryKey] = new List<string> { dictionaryListValue } };
|
||||
this.Api.EnumProperty = enumValue;
|
||||
this.Api.InheritedProperty = inheritedValue;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
This project contains simulated [mod-provided APIs] used in the API proxying unit tests.
|
||||
|
||||
[mod-provided APIs]: https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Integrations
|
|
@ -0,0 +1,11 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="..\..\build\common.targets" />
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SMAPI\SMAPI.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
|
@ -0,0 +1,294 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using StardewModdingAPI;
|
||||
using StardewModdingAPI.Framework.Content;
|
||||
using StardewModdingAPI.Toolkit.Utilities;
|
||||
using StardewValley;
|
||||
|
||||
namespace SMAPI.Tests.Core
|
||||
{
|
||||
/// <summary>Unit tests for <see cref="AssetName"/>.</summary>
|
||||
[TestFixture]
|
||||
internal class AssetNameTests
|
||||
{
|
||||
/*********
|
||||
** Unit tests
|
||||
*********/
|
||||
/****
|
||||
** Constructor
|
||||
****/
|
||||
[Test(Description = $"Assert that the {nameof(AssetName)} constructor creates an instance with the expected values.")]
|
||||
[TestCase("SimpleName", "SimpleName", null, null)]
|
||||
[TestCase("Data/Achievements", "Data/Achievements", null, null)]
|
||||
[TestCase("Characters/Dialogue/Abigail", "Characters/Dialogue/Abigail", null, null)]
|
||||
[TestCase("Characters/Dialogue/Abigail.fr-FR", "Characters/Dialogue/Abigail", "fr-FR", LocalizedContentManager.LanguageCode.fr)]
|
||||
[TestCase("Characters/Dialogue\\Abigail.fr-FR", "Characters/Dialogue/Abigail.fr-FR", null, null)]
|
||||
[TestCase("Characters/Dialogue/Abigail.fr-FR", "Characters/Dialogue/Abigail", "fr-FR", LocalizedContentManager.LanguageCode.fr)]
|
||||
public void Constructor_Valid(string name, string expectedBaseName, string? expectedLocale, LocalizedContentManager.LanguageCode? expectedLanguageCode)
|
||||
{
|
||||
// arrange
|
||||
name = PathUtilities.NormalizeAssetName(name);
|
||||
|
||||
// act
|
||||
IAssetName assetName = AssetName.Parse(name, parseLocale: _ => expectedLanguageCode);
|
||||
|
||||
// assert
|
||||
assetName.Name.Should()
|
||||
.NotBeNull()
|
||||
.And.Be(name.Replace("\\", "/"));
|
||||
assetName.BaseName.Should()
|
||||
.NotBeNull()
|
||||
.And.Be(expectedBaseName);
|
||||
assetName.LocaleCode.Should()
|
||||
.Be(expectedLocale);
|
||||
assetName.LanguageCode.Should()
|
||||
.Be(expectedLanguageCode);
|
||||
}
|
||||
|
||||
[Test(Description = $"Assert that the {nameof(AssetName)} constructor throws an exception if the value is invalid.")]
|
||||
[TestCase(null)]
|
||||
[TestCase("")]
|
||||
[TestCase(" ")]
|
||||
[TestCase("\t")]
|
||||
[TestCase(" \t ")]
|
||||
public void Constructor_NullOrWhitespace(string? name)
|
||||
{
|
||||
// act
|
||||
ArgumentException exception = Assert.Throws<ArgumentException>(() => _ = AssetName.Parse(name!, _ => null))!;
|
||||
|
||||
// assert
|
||||
exception.ParamName.Should().Be("rawName");
|
||||
exception.Message.Should().Be("The asset name can't be null or empty. (Parameter 'rawName')");
|
||||
}
|
||||
|
||||
|
||||
/****
|
||||
** IsEquivalentTo
|
||||
****/
|
||||
[Test(Description = $"Assert that {nameof(AssetName.IsEquivalentTo)} compares names as expected when the locale is included.")]
|
||||
|
||||
// exact match (ignore case)
|
||||
[TestCase("Data/Achievements", "Data/Achievements", ExpectedResult = true)]
|
||||
[TestCase("DATA/achievements", "data/ACHIEVEMENTS", ExpectedResult = true)]
|
||||
|
||||
// exact match (ignore formatting)
|
||||
[TestCase("Data/Achievements", "Data\\Achievements", ExpectedResult = true)]
|
||||
[TestCase("DATA\\achievements", "data/ACHIEVEMENTS", ExpectedResult = true)]
|
||||
[TestCase("DATA\\\\achievements", "data////ACHIEVEMENTS", ExpectedResult = true)]
|
||||
|
||||
// whitespace-insensitive
|
||||
[TestCase("Data/Achievements", " Data/Achievements ", ExpectedResult = true)]
|
||||
[TestCase(" Data/Achievements ", "Data/Achievements", ExpectedResult = true)]
|
||||
|
||||
// other is null or whitespace
|
||||
[TestCase("Data/Achievements", null, ExpectedResult = false)]
|
||||
[TestCase("Data/Achievements", "", ExpectedResult = false)]
|
||||
[TestCase("Data/Achievements", " ", ExpectedResult = false)]
|
||||
|
||||
// with locale codes
|
||||
[TestCase("Data/Achievements", "Data/Achievements.fr-FR", ExpectedResult = false)]
|
||||
[TestCase("Data/Achievements.fr-FR", "Data/Achievements", ExpectedResult = false)]
|
||||
[TestCase("Data/Achievements.fr-FR", "Data/Achievements.fr-FR", ExpectedResult = true)]
|
||||
public bool IsEquivalentTo_Name(string mainAssetName, string otherAssetName)
|
||||
{
|
||||
// arrange
|
||||
mainAssetName = PathUtilities.NormalizeAssetName(mainAssetName);
|
||||
|
||||
// act
|
||||
AssetName name = AssetName.Parse(mainAssetName, _ => LocalizedContentManager.LanguageCode.fr);
|
||||
|
||||
// assert
|
||||
return name.IsEquivalentTo(otherAssetName);
|
||||
}
|
||||
|
||||
[Test(Description = $"Assert that {nameof(AssetName.IsEquivalentTo)} compares names as expected when the locale is excluded.")]
|
||||
|
||||
// a few samples from previous test to make sure
|
||||
[TestCase("Data/Achievements", "Data/Achievements", ExpectedResult = true)]
|
||||
[TestCase("DATA/achievements", "data/ACHIEVEMENTS", ExpectedResult = true)]
|
||||
[TestCase("DATA\\\\achievements", "data////ACHIEVEMENTS", ExpectedResult = true)]
|
||||
[TestCase(" Data/Achievements ", "Data/Achievements", ExpectedResult = true)]
|
||||
[TestCase("Data/Achievements", " ", ExpectedResult = false)]
|
||||
|
||||
// with locale codes
|
||||
[TestCase("Data/Achievements", "Data/Achievements.fr-FR", ExpectedResult = false)]
|
||||
[TestCase("Data/Achievements.fr-FR", "Data/Achievements", ExpectedResult = true)]
|
||||
[TestCase("Data/Achievements.fr-FR", "Data/Achievements.fr-FR", ExpectedResult = false)]
|
||||
public bool IsEquivalentTo_BaseName(string mainAssetName, string otherAssetName)
|
||||
{
|
||||
// arrange
|
||||
mainAssetName = PathUtilities.NormalizeAssetName(mainAssetName);
|
||||
|
||||
// act
|
||||
AssetName name = AssetName.Parse(mainAssetName, _ => LocalizedContentManager.LanguageCode.fr);
|
||||
|
||||
// assert
|
||||
return name.IsEquivalentTo(otherAssetName, useBaseName: true);
|
||||
}
|
||||
|
||||
|
||||
/****
|
||||
** StartsWith
|
||||
****/
|
||||
[Test(Description = $"Assert that {nameof(AssetName.StartsWith)} compares names as expected for inputs that aren't affected by the input options.")]
|
||||
|
||||
// exact match (ignore case and formatting)
|
||||
[TestCase("Data/Achievements", "Data/Achievements", ExpectedResult = true)]
|
||||
[TestCase("DATA/achievements", "data/ACHIEVEMENTS", ExpectedResult = true)]
|
||||
[TestCase("Data/Achievements", "Data\\Achievements", ExpectedResult = true)]
|
||||
[TestCase("DATA\\achievements", "data/ACHIEVEMENTS", ExpectedResult = true)]
|
||||
[TestCase("DATA\\\\achievements", "data////ACHIEVEMENTS", ExpectedResult = true)]
|
||||
|
||||
// whitespace-insensitive
|
||||
[TestCase("Data/Achievements", " Data/Achievements", ExpectedResult = true)]
|
||||
[TestCase(" Data/Achievements ", "Data/Achievements", ExpectedResult = true)]
|
||||
[TestCase("Data/Achievements", " ", ExpectedResult = true)]
|
||||
|
||||
// invalid prefixes
|
||||
[TestCase("Data/Achievements", null, ExpectedResult = false)]
|
||||
|
||||
// with locale codes
|
||||
[TestCase("Data/Achievements.fr-FR", "Data/Achievements", ExpectedResult = true)]
|
||||
public bool StartsWith_SimpleCases(string mainAssetName, string prefix)
|
||||
{
|
||||
// arrange
|
||||
mainAssetName = PathUtilities.NormalizeAssetName(mainAssetName);
|
||||
|
||||
// act
|
||||
AssetName name = AssetName.Parse(mainAssetName, _ => null);
|
||||
|
||||
// assert value is the same for any combination of options
|
||||
bool result = name.StartsWith(prefix);
|
||||
foreach (bool allowPartialWord in new[] { true, false })
|
||||
{
|
||||
foreach (bool allowSubfolder in new[] { true, true })
|
||||
{
|
||||
if (allowPartialWord && allowSubfolder)
|
||||
continue;
|
||||
|
||||
name.StartsWith(prefix, allowPartialWord, allowSubfolder)
|
||||
.Should().Be(result, $"the value returned for options ({nameof(allowPartialWord)}: {allowPartialWord}, {nameof(allowSubfolder)}: {allowSubfolder}) should match the base case");
|
||||
}
|
||||
}
|
||||
|
||||
// assert value
|
||||
return result;
|
||||
}
|
||||
|
||||
[Test(Description = $"Assert that {nameof(AssetName.StartsWith)} compares names as expected for the 'allowPartialWord' option.")]
|
||||
[TestCase("Data/AchievementsToIgnore", "Data/Achievements", true, ExpectedResult = true)]
|
||||
[TestCase("Data/AchievementsToIgnore", "Data/Achievements", false, ExpectedResult = false)]
|
||||
[TestCase("Data/Achievements X", "Data/Achievements", true, ExpectedResult = true)]
|
||||
[TestCase("Data/Achievements X", "Data/Achievements", false, ExpectedResult = true)]
|
||||
[TestCase("Data/Achievements.X", "Data/Achievements", true, ExpectedResult = true)]
|
||||
[TestCase("Data/Achievements.X", "Data/Achievements", false, ExpectedResult = true)]
|
||||
|
||||
// with locale codes
|
||||
[TestCase("Data/Achievements.fr-FR", "Data/Achievements", true, ExpectedResult = true)]
|
||||
[TestCase("Data/Achievements.fr-FR", "Data/Achievements", false, ExpectedResult = true)]
|
||||
public bool StartsWith_PartialWord(string mainAssetName, string prefix, bool allowPartialWord)
|
||||
{
|
||||
// arrange
|
||||
mainAssetName = PathUtilities.NormalizeAssetName(mainAssetName);
|
||||
|
||||
// act
|
||||
AssetName name = AssetName.Parse(mainAssetName, _ => null);
|
||||
|
||||
// assert value is the same for any combination of options
|
||||
bool result = name.StartsWith(prefix, allowPartialWord: allowPartialWord, allowSubfolder: true);
|
||||
name.StartsWith(prefix, allowPartialWord, allowSubfolder: false)
|
||||
.Should().Be(result, "specifying allowSubfolder should have no effect for these inputs");
|
||||
|
||||
// assert value
|
||||
return result;
|
||||
}
|
||||
|
||||
[Test(Description = $"Assert that {nameof(AssetName.StartsWith)} compares names as expected for the 'allowSubfolder' option.")]
|
||||
|
||||
// simple cases
|
||||
[TestCase("Data/Achievements/Path", "Data/Achievements", true, ExpectedResult = true)]
|
||||
[TestCase("Data/Achievements/Path", "Data/Achievements", false, ExpectedResult = false)]
|
||||
[TestCase("Data/Achievements/Path", "Data\\Achievements", true, ExpectedResult = true)]
|
||||
[TestCase("Data/Achievements/Path", "Data\\Achievements", false, ExpectedResult = false)]
|
||||
|
||||
// trailing slash
|
||||
[TestCase("Data/Achievements/Path", "Data/", true, ExpectedResult = true)]
|
||||
[TestCase("Data/Achievements/Path", "Data/", false, ExpectedResult = false)]
|
||||
|
||||
// normalize slash style
|
||||
[TestCase("Data/Achievements/Path", "Data\\", true, ExpectedResult = true)]
|
||||
[TestCase("Data/Achievements/Path", "Data\\", false, ExpectedResult = false)]
|
||||
[TestCase("Data/Achievements/Path", "Data/\\/", true, ExpectedResult = true)]
|
||||
[TestCase("Data/Achievements/Path", "Data/\\/", false, ExpectedResult = false)]
|
||||
|
||||
// with locale code
|
||||
[TestCase("Data/Achievements/Path.fr-FR", "Data/Achievements", true, ExpectedResult = true)]
|
||||
[TestCase("Data/Achievements/Path.fr-FR", "Data/Achievements", false, ExpectedResult = false)]
|
||||
public bool StartsWith_Subfolder(string mainAssetName, string otherAssetName, bool allowSubfolder)
|
||||
{
|
||||
// arrange
|
||||
mainAssetName = PathUtilities.NormalizeAssetName(mainAssetName);
|
||||
|
||||
// act
|
||||
AssetName name = AssetName.Parse(mainAssetName, _ => null);
|
||||
|
||||
// assert value is the same for any combination of options
|
||||
bool result = name.StartsWith(otherAssetName, allowPartialWord: true, allowSubfolder: allowSubfolder);
|
||||
name.StartsWith(otherAssetName, allowPartialWord: false, allowSubfolder: allowSubfolder)
|
||||
.Should().Be(result, "specifying allowPartialWord should have no effect for these inputs");
|
||||
|
||||
// assert value
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/****
|
||||
** GetHashCode
|
||||
****/
|
||||
[Test(Description = $"Assert that {nameof(AssetName.GetHashCode)} generates the same hash code for two asset names which differ only by capitalization.")]
|
||||
public void GetHashCode_IsCaseInsensitive()
|
||||
{
|
||||
// arrange
|
||||
string left = "data/ACHIEVEMENTS";
|
||||
string right = "DATA/achievements";
|
||||
|
||||
// act
|
||||
int leftHash = AssetName.Parse(left, _ => null).GetHashCode();
|
||||
int rightHash = AssetName.Parse(right, _ => null).GetHashCode();
|
||||
|
||||
// assert
|
||||
leftHash.Should().Be(rightHash, "two asset names which differ only by capitalization should produce the same hash code");
|
||||
}
|
||||
|
||||
[Test(Description = $"Assert that {nameof(AssetName.GetHashCode)} generates few hash code collisions for an arbitrary set of asset names.")]
|
||||
public void GetHashCode_HasFewCollisions()
|
||||
{
|
||||
// generate list of names
|
||||
List<string> names = new();
|
||||
{
|
||||
Random random = new();
|
||||
string characters = "abcdefghijklmnopqrstuvwxyz1234567890/";
|
||||
|
||||
while (names.Count < 1000)
|
||||
{
|
||||
char[] name = new char[random.Next(5, 20)];
|
||||
for (int i = 0; i < name.Length; i++)
|
||||
name[i] = characters[random.Next(0, characters.Length)];
|
||||
|
||||
names.Add(new string(name));
|
||||
}
|
||||
}
|
||||
|
||||
// get distinct hash codes
|
||||
HashSet<int> hashCodes = new();
|
||||
foreach (string name in names)
|
||||
hashCodes.Add(AssetName.Parse(name, _ => null).GetHashCode());
|
||||
|
||||
// assert a collision frequency under 0.1%
|
||||
float collisionFrequency = 1 - (hashCodes.Count / (names.Count * 1f));
|
||||
collisionFrequency.Should().BeLessOrEqualTo(0.001f, "hash codes should be relatively distinct with a collision rate under 0.1% for a small sample set");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,400 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using SMAPI.Tests.ModApiConsumer;
|
||||
using SMAPI.Tests.ModApiConsumer.Interfaces;
|
||||
using SMAPI.Tests.ModApiProvider;
|
||||
using StardewModdingAPI.Framework.Reflection;
|
||||
using StardewModdingAPI.Utilities;
|
||||
|
||||
namespace SMAPI.Tests.Core
|
||||
{
|
||||
/// <summary>Unit tests for <see cref="InterfaceProxyFactory"/>.</summary>
|
||||
[TestFixture]
|
||||
internal class InterfaceProxyTests
|
||||
{
|
||||
/*********
|
||||
** Fields
|
||||
*********/
|
||||
/// <summary>The mod ID providing an API.</summary>
|
||||
private readonly string FromModId = "From.ModId";
|
||||
|
||||
/// <summary>The mod ID consuming an API.</summary>
|
||||
private readonly string ToModId = "From.ModId";
|
||||
|
||||
/// <summary>The random number generator with which to create sample values.</summary>
|
||||
private readonly Random Random = new();
|
||||
|
||||
/// <summary>Sample user inputs for season names.</summary>
|
||||
private static readonly IInterfaceProxyFactory[] ProxyFactories = {
|
||||
new InterfaceProxyFactory(),
|
||||
new OriginalInterfaceProxyFactory()
|
||||
};
|
||||
|
||||
|
||||
/*********
|
||||
** Unit tests
|
||||
*********/
|
||||
/****
|
||||
** Events
|
||||
****/
|
||||
/// <summary>Assert that an event field can be proxied correctly.</summary>
|
||||
/// <param name="proxyFactory">The proxy factory to test.</param>
|
||||
[Test]
|
||||
public void CanProxy_EventField([ValueSource(nameof(InterfaceProxyTests.ProxyFactories))] IInterfaceProxyFactory proxyFactory)
|
||||
{
|
||||
// arrange
|
||||
ProviderMod providerMod = new();
|
||||
object implementation = providerMod.GetModApi();
|
||||
int expectedValue = this.Random.Next();
|
||||
|
||||
// act
|
||||
ISimpleApi proxy = this.GetProxy(proxyFactory, implementation);
|
||||
new ApiConsumer().UseEventField(proxy, out Func<(int timesCalled, int lastValue)> getValues);
|
||||
providerMod.RaiseEvent(expectedValue);
|
||||
(int timesCalled, int lastValue) = getValues();
|
||||
|
||||
// assert
|
||||
timesCalled.Should().Be(1, "Expected the proxied event to be raised once.");
|
||||
lastValue.Should().Be(expectedValue, "The proxy received a different event argument than the implementation raised.");
|
||||
}
|
||||
|
||||
/// <summary>Assert that an event property can be proxied correctly.</summary>
|
||||
/// <param name="proxyFactory">The proxy factory to test.</param>
|
||||
[Test]
|
||||
public void CanProxy_EventProperty([ValueSource(nameof(InterfaceProxyTests.ProxyFactories))] IInterfaceProxyFactory proxyFactory)
|
||||
{
|
||||
// arrange
|
||||
ProviderMod providerMod = new();
|
||||
object implementation = providerMod.GetModApi();
|
||||
int expectedValue = this.Random.Next();
|
||||
|
||||
// act
|
||||
ISimpleApi proxy = this.GetProxy(proxyFactory, implementation);
|
||||
new ApiConsumer().UseEventProperty(proxy, out Func<(int timesCalled, int lastValue)> getValues);
|
||||
providerMod.RaiseEvent(expectedValue);
|
||||
(int timesCalled, int lastValue) = getValues();
|
||||
|
||||
// assert
|
||||
timesCalled.Should().Be(1, "Expected the proxied event to be raised once.");
|
||||
lastValue.Should().Be(expectedValue, "The proxy received a different event argument than the implementation raised.");
|
||||
}
|
||||
|
||||
/****
|
||||
** Properties
|
||||
****/
|
||||
/// <summary>Assert that properties can be proxied correctly.</summary>
|
||||
/// <param name="proxyFactory">The proxy factory to test.</param>
|
||||
/// <param name="setVia">Whether to set the properties through the <c>provider mod</c> or <c>proxy interface</c>.</param>
|
||||
[Test]
|
||||
public void CanProxy_Properties([ValueSource(nameof(InterfaceProxyTests.ProxyFactories))] IInterfaceProxyFactory proxyFactory, [Values("set via provider mod", "set via proxy interface")] string setVia)
|
||||
{
|
||||
// arrange
|
||||
ProviderMod providerMod = new();
|
||||
object implementation = providerMod.GetModApi();
|
||||
int expectedNumber = this.Random.Next();
|
||||
int expectedObject = this.Random.Next();
|
||||
string expectedListValue = this.GetRandomString();
|
||||
string expectedListWithInterfaceValue = this.GetRandomString();
|
||||
string expectedDictionaryKey = this.GetRandomString();
|
||||
string expectedDictionaryListValue = this.GetRandomString();
|
||||
string expectedInheritedString = this.GetRandomString();
|
||||
BindingFlags expectedEnum = BindingFlags.Instance | BindingFlags.Public;
|
||||
|
||||
// act
|
||||
ISimpleApi proxy = this.GetProxy(proxyFactory, implementation);
|
||||
switch (setVia)
|
||||
{
|
||||
case "set via provider mod":
|
||||
providerMod.SetPropertyValues(
|
||||
number: expectedNumber,
|
||||
obj: expectedObject,
|
||||
listValue: expectedListValue,
|
||||
listWithInterfaceValue: expectedListWithInterfaceValue,
|
||||
dictionaryKey: expectedDictionaryKey,
|
||||
dictionaryListValue: expectedDictionaryListValue,
|
||||
enumValue: expectedEnum,
|
||||
inheritedValue: expectedInheritedString
|
||||
);
|
||||
break;
|
||||
|
||||
case "set via proxy interface":
|
||||
proxy.NumberProperty = expectedNumber;
|
||||
proxy.ObjectProperty = expectedObject;
|
||||
proxy.ListProperty = new() { expectedListValue };
|
||||
proxy.ListPropertyWithInterface = new List<string> { expectedListWithInterfaceValue };
|
||||
proxy.GenericsProperty = new Dictionary<string, IList<string>>
|
||||
{
|
||||
[expectedDictionaryKey] = new List<string> { expectedDictionaryListValue }
|
||||
};
|
||||
proxy.EnumProperty = expectedEnum;
|
||||
proxy.InheritedProperty = expectedInheritedString;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new InvalidOperationException($"Invalid 'set via' option '{setVia}.");
|
||||
}
|
||||
|
||||
// assert number
|
||||
this
|
||||
.GetPropertyValue(implementation, nameof(proxy.NumberProperty))
|
||||
.Should().Be(expectedNumber);
|
||||
proxy.NumberProperty
|
||||
.Should().Be(expectedNumber);
|
||||
|
||||
// assert object
|
||||
this
|
||||
.GetPropertyValue(implementation, nameof(proxy.ObjectProperty))
|
||||
.Should().Be(expectedObject);
|
||||
proxy.ObjectProperty
|
||||
.Should().Be(expectedObject);
|
||||
|
||||
// assert list
|
||||
(this.GetPropertyValue(implementation, nameof(proxy.ListProperty)) as IList<string>)
|
||||
.Should().NotBeNull()
|
||||
.And.HaveCount(1)
|
||||
.And.BeEquivalentTo(expectedListValue);
|
||||
proxy.ListProperty
|
||||
.Should().NotBeNull()
|
||||
.And.HaveCount(1)
|
||||
.And.BeEquivalentTo(expectedListValue);
|
||||
|
||||
// assert list with interface
|
||||
(this.GetPropertyValue(implementation, nameof(proxy.ListPropertyWithInterface)) as IList<string>)
|
||||
.Should().NotBeNull()
|
||||
.And.HaveCount(1)
|
||||
.And.BeEquivalentTo(expectedListWithInterfaceValue);
|
||||
proxy.ListPropertyWithInterface
|
||||
.Should().NotBeNull()
|
||||
.And.HaveCount(1)
|
||||
.And.BeEquivalentTo(expectedListWithInterfaceValue);
|
||||
|
||||
// assert generics
|
||||
(this.GetPropertyValue(implementation, nameof(proxy.GenericsProperty)) as IDictionary<string, IList<string>>)
|
||||
.Should().NotBeNull()
|
||||
.And.HaveCount(1)
|
||||
.And.ContainKey(expectedDictionaryKey).WhoseValue.Should().BeEquivalentTo(expectedDictionaryListValue);
|
||||
proxy.GenericsProperty
|
||||
.Should().NotBeNull()
|
||||
.And.HaveCount(1)
|
||||
.And.ContainKey(expectedDictionaryKey).WhoseValue.Should().BeEquivalentTo(expectedDictionaryListValue);
|
||||
|
||||
// assert enum
|
||||
this
|
||||
.GetPropertyValue(implementation, nameof(proxy.EnumProperty))
|
||||
.Should().Be(expectedEnum);
|
||||
proxy.EnumProperty
|
||||
.Should().Be(expectedEnum);
|
||||
|
||||
// assert getter
|
||||
this
|
||||
.GetPropertyValue(implementation, nameof(proxy.GetterProperty))
|
||||
.Should().Be(42);
|
||||
proxy.GetterProperty
|
||||
.Should().Be(42);
|
||||
|
||||
// assert inherited methods
|
||||
this
|
||||
.GetPropertyValue(implementation, nameof(proxy.InheritedProperty))
|
||||
.Should().Be(expectedInheritedString);
|
||||
proxy.InheritedProperty
|
||||
.Should().Be(expectedInheritedString);
|
||||
}
|
||||
|
||||
/// <summary>Assert that a simple method with no return value can be proxied correctly.</summary>
|
||||
/// <param name="proxyFactory">The proxy factory to test.</param>
|
||||
[Test]
|
||||
public void CanProxy_SimpleMethod_Void([ValueSource(nameof(InterfaceProxyTests.ProxyFactories))] IInterfaceProxyFactory proxyFactory)
|
||||
{
|
||||
// arrange
|
||||
object implementation = new ProviderMod().GetModApi();
|
||||
|
||||
// act
|
||||
ISimpleApi proxy = this.GetProxy(proxyFactory, implementation);
|
||||
proxy.GetNothing();
|
||||
}
|
||||
|
||||
/// <summary>Assert that a simple int method can be proxied correctly.</summary>
|
||||
/// <param name="proxyFactory">The proxy factory to test.</param>
|
||||
[Test]
|
||||
public void CanProxy_SimpleMethod_Int([ValueSource(nameof(InterfaceProxyTests.ProxyFactories))] IInterfaceProxyFactory proxyFactory)
|
||||
{
|
||||
// arrange
|
||||
object implementation = new ProviderMod().GetModApi();
|
||||
int expectedValue = this.Random.Next();
|
||||
|
||||
// act
|
||||
ISimpleApi proxy = this.GetProxy(proxyFactory, implementation);
|
||||
int actualValue = proxy.GetInt(expectedValue);
|
||||
|
||||
// assert
|
||||
actualValue.Should().Be(expectedValue);
|
||||
}
|
||||
|
||||
/// <summary>Assert that a simple object method can be proxied correctly.</summary>
|
||||
/// <param name="proxyFactory">The proxy factory to test.</param>
|
||||
[Test]
|
||||
public void CanProxy_SimpleMethod_Object([ValueSource(nameof(InterfaceProxyTests.ProxyFactories))] IInterfaceProxyFactory proxyFactory)
|
||||
{
|
||||
// arrange
|
||||
object implementation = new ProviderMod().GetModApi();
|
||||
object expectedValue = new();
|
||||
|
||||
// act
|
||||
ISimpleApi proxy = this.GetProxy(proxyFactory, implementation);
|
||||
object actualValue = proxy.GetObject(expectedValue);
|
||||
|
||||
// assert
|
||||
actualValue.Should().BeSameAs(expectedValue);
|
||||
}
|
||||
|
||||
/// <summary>Assert that a simple list method can be proxied correctly.</summary>
|
||||
/// <param name="proxyFactory">The proxy factory to test.</param>
|
||||
[Test]
|
||||
public void CanProxy_SimpleMethod_List([ValueSource(nameof(InterfaceProxyTests.ProxyFactories))] IInterfaceProxyFactory proxyFactory)
|
||||
{
|
||||
// arrange
|
||||
object implementation = new ProviderMod().GetModApi();
|
||||
string expectedValue = this.GetRandomString();
|
||||
|
||||
// act
|
||||
ISimpleApi proxy = this.GetProxy(proxyFactory, implementation);
|
||||
IList<string> actualValue = proxy.GetList(expectedValue);
|
||||
|
||||
// assert
|
||||
actualValue.Should().BeEquivalentTo(expectedValue);
|
||||
}
|
||||
|
||||
/// <summary>Assert that a simple list with interface method can be proxied correctly.</summary>
|
||||
/// <param name="proxyFactory">The proxy factory to test.</param>
|
||||
[Test]
|
||||
public void CanProxy_SimpleMethod_ListWithInterface([ValueSource(nameof(InterfaceProxyTests.ProxyFactories))] IInterfaceProxyFactory proxyFactory)
|
||||
{
|
||||
// arrange
|
||||
object implementation = new ProviderMod().GetModApi();
|
||||
string expectedValue = this.GetRandomString();
|
||||
|
||||
// act
|
||||
ISimpleApi proxy = this.GetProxy(proxyFactory, implementation);
|
||||
IList<string> actualValue = proxy.GetListWithInterface(expectedValue);
|
||||
|
||||
// assert
|
||||
actualValue.Should().BeEquivalentTo(expectedValue);
|
||||
}
|
||||
|
||||
/// <summary>Assert that a simple method which returns generic types can be proxied correctly.</summary>
|
||||
/// <param name="proxyFactory">The proxy factory to test.</param>
|
||||
[Test]
|
||||
public void CanProxy_SimpleMethod_GenericTypes([ValueSource(nameof(InterfaceProxyTests.ProxyFactories))] IInterfaceProxyFactory proxyFactory)
|
||||
{
|
||||
// arrange
|
||||
object implementation = new ProviderMod().GetModApi();
|
||||
string expectedKey = this.GetRandomString();
|
||||
string expectedValue = this.GetRandomString();
|
||||
|
||||
// act
|
||||
ISimpleApi proxy = this.GetProxy(proxyFactory, implementation);
|
||||
IDictionary<string, IList<string>> actualValue = proxy.GetGenerics(expectedKey, expectedValue);
|
||||
|
||||
// assert
|
||||
actualValue
|
||||
.Should().NotBeNull()
|
||||
.And.HaveCount(1)
|
||||
.And.ContainKey(expectedKey).WhoseValue.Should().BeEquivalentTo(expectedValue);
|
||||
}
|
||||
|
||||
/// <summary>Assert that a simple lambda method can be proxied correctly.</summary>
|
||||
/// <param name="proxyFactory">The proxy factory to test.</param>
|
||||
[Test]
|
||||
[SuppressMessage("ReSharper", "ConvertToLocalFunction")]
|
||||
public void CanProxy_SimpleMethod_Lambda([ValueSource(nameof(InterfaceProxyTests.ProxyFactories))] IInterfaceProxyFactory proxyFactory)
|
||||
{
|
||||
// arrange
|
||||
object implementation = new ProviderMod().GetModApi();
|
||||
Func<string, string> expectedValue = _ => "test";
|
||||
|
||||
// act
|
||||
ISimpleApi proxy = this.GetProxy(proxyFactory, implementation);
|
||||
object actualValue = proxy.GetObject(expectedValue);
|
||||
|
||||
// assert
|
||||
actualValue.Should().BeSameAs(expectedValue);
|
||||
}
|
||||
|
||||
/// <summary>Assert that a method with out parameters can be proxied correctly.</summary>
|
||||
/// <param name="proxyFactory">The proxy factory to test.</param>
|
||||
[Test]
|
||||
[SuppressMessage("ReSharper", "ConvertToLocalFunction")]
|
||||
public void CanProxy_Method_OutParameters([ValueSource(nameof(InterfaceProxyTests.ProxyFactories))] IInterfaceProxyFactory proxyFactory)
|
||||
{
|
||||
// arrange
|
||||
object implementation = new ProviderMod().GetModApi();
|
||||
const int expectedNumber = 42;
|
||||
|
||||
// act
|
||||
ISimpleApi proxy = this.GetProxy(proxyFactory, implementation);
|
||||
bool result = proxy.TryGetOutParameter(
|
||||
inputNumber: expectedNumber,
|
||||
|
||||
out int outNumber,
|
||||
out string outString,
|
||||
out PerScreen<int> outReference,
|
||||
out IDictionary<int, PerScreen<int>> outComplexType
|
||||
);
|
||||
|
||||
// assert
|
||||
result.Should().BeTrue();
|
||||
|
||||
outNumber.Should().Be(expectedNumber);
|
||||
|
||||
outString.Should().Be(expectedNumber.ToString());
|
||||
|
||||
outReference.Should().NotBeNull();
|
||||
outReference.Value.Should().Be(expectedNumber);
|
||||
|
||||
outComplexType.Should().NotBeNull();
|
||||
outComplexType.Count.Should().Be(1);
|
||||
outComplexType.Keys.First().Should().Be(expectedNumber);
|
||||
outComplexType.Values.First().Should().NotBeNull();
|
||||
outComplexType.Values.First().Value.Should().Be(expectedNumber);
|
||||
}
|
||||
|
||||
|
||||
/*********
|
||||
** Private methods
|
||||
*********/
|
||||
/// <summary>Get a property value from an instance.</summary>
|
||||
/// <param name="parent">The instance whose property to read.</param>
|
||||
/// <param name="name">The property name.</param>
|
||||
private object? GetPropertyValue(object parent, string name)
|
||||
{
|
||||
if (parent is null)
|
||||
throw new ArgumentNullException(nameof(parent));
|
||||
|
||||
Type type = parent.GetType();
|
||||
PropertyInfo? property = type.GetProperty(name);
|
||||
if (property is null)
|
||||
throw new InvalidOperationException($"The '{type.FullName}' type has no public property named '{name}'.");
|
||||
|
||||
return property.GetValue(parent);
|
||||
}
|
||||
|
||||
/// <summary>Get a random test string.</summary>
|
||||
private string GetRandomString()
|
||||
{
|
||||
return this.Random.Next().ToString();
|
||||
}
|
||||
|
||||
/// <summary>Get a proxy API instance.</summary>
|
||||
/// <param name="proxyFactory">The proxy factory to use.</param>
|
||||
/// <param name="implementation">The underlying API instance.</param>
|
||||
private ISimpleApi GetProxy(IInterfaceProxyFactory proxyFactory, object implementation)
|
||||
{
|
||||
return proxyFactory.CreateProxy<ISimpleApi>(implementation, this.FromModId, this.ToModId);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -38,6 +38,9 @@ namespace SMAPI.Tests.Core
|
|||
|
||||
// assert
|
||||
Assert.AreEqual(0, mods.Length, 0, $"Expected to find zero manifests, found {mods.Length} instead.");
|
||||
|
||||
// cleanup
|
||||
Directory.Delete(rootFolder, recursive: true);
|
||||
}
|
||||
|
||||
[Test(Description = "Assert that the resolver correctly returns a failed metadata if there's an empty mod folder.")]
|
||||
|
@ -50,12 +53,15 @@ namespace SMAPI.Tests.Core
|
|||
|
||||
// act
|
||||
IModMetadata[] mods = new ModResolver().ReadManifests(new ModToolkit(), rootFolder, new ModDatabase()).ToArray();
|
||||
IModMetadata mod = mods.FirstOrDefault();
|
||||
IModMetadata? mod = mods.FirstOrDefault();
|
||||
|
||||
// assert
|
||||
Assert.AreEqual(1, mods.Length, 0, $"Expected to find one manifest, found {mods.Length} instead.");
|
||||
Assert.AreEqual(ModMetadataStatus.Failed, mod.Status, "The mod metadata was not marked failed.");
|
||||
Assert.AreEqual(ModMetadataStatus.Failed, mod!.Status, "The mod metadata was not marked failed.");
|
||||
Assert.IsNotNull(mod.Error, "The mod metadata did not have an error message set.");
|
||||
|
||||
// cleanup
|
||||
Directory.Delete(rootFolder, recursive: true);
|
||||
}
|
||||
|
||||
[Test(Description = "Assert that the resolver correctly reads manifest data from a randomized file.")]
|
||||
|
@ -89,12 +95,12 @@ namespace SMAPI.Tests.Core
|
|||
|
||||
// act
|
||||
IModMetadata[] mods = new ModResolver().ReadManifests(new ModToolkit(), rootFolder, new ModDatabase()).ToArray();
|
||||
IModMetadata mod = mods.FirstOrDefault();
|
||||
IModMetadata? mod = mods.FirstOrDefault();
|
||||
|
||||
// assert
|
||||
Assert.AreEqual(1, mods.Length, 0, "Expected to find one manifest.");
|
||||
Assert.IsNotNull(mod, "The loaded manifest shouldn't be null.");
|
||||
Assert.AreEqual(null, mod.DataRecord, "The data record should be null since we didn't provide one.");
|
||||
Assert.AreEqual(null, mod!.DataRecord, "The data record should be null since we didn't provide one.");
|
||||
Assert.AreEqual(modFolder, mod.DirectoryPath, "The directory path doesn't match.");
|
||||
Assert.AreEqual(null, mod.Error, "The error should be null since parsing should have succeeded.");
|
||||
Assert.AreEqual(ModMetadataStatus.Found, mod.Status, "The status doesn't match.");
|
||||
|
@ -115,6 +121,9 @@ namespace SMAPI.Tests.Core
|
|||
Assert.IsNotNull(mod.Manifest.Dependencies, "The dependencies field should not be null.");
|
||||
Assert.AreEqual(1, mod.Manifest.Dependencies.Length, "The dependencies field should contain one value.");
|
||||
Assert.AreEqual(originalDependency[nameof(IManifestDependency.UniqueID)], mod.Manifest.Dependencies[0].UniqueID, "The first dependency's unique ID doesn't match.");
|
||||
|
||||
// cleanup
|
||||
Directory.Delete(rootFolder, recursive: true);
|
||||
}
|
||||
|
||||
/****
|
||||
|
@ -123,7 +132,7 @@ namespace SMAPI.Tests.Core
|
|||
[Test(Description = "Assert that validation doesn't fail if there are no mods installed.")]
|
||||
public void ValidateManifests_NoMods_DoesNothing()
|
||||
{
|
||||
new ModResolver().ValidateManifests(new ModMetadata[0], apiVersion: new SemanticVersion("1.0"), getUpdateUrl: key => null);
|
||||
new ModResolver().ValidateManifests(Array.Empty<ModMetadata>(), apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null, validateFilesExist: false);
|
||||
}
|
||||
|
||||
[Test(Description = "Assert that validation skips manifests that have already failed without calling any other properties.")]
|
||||
|
@ -134,7 +143,7 @@ namespace SMAPI.Tests.Core
|
|||
mock.Setup(p => p.Status).Returns(ModMetadataStatus.Failed);
|
||||
|
||||
// act
|
||||
new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: key => null);
|
||||
new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null, validateFilesExist: false);
|
||||
|
||||
// assert
|
||||
mock.VerifyGet(p => p.Status, Times.Once, "The validation did not check the manifest status.");
|
||||
|
@ -144,14 +153,14 @@ namespace SMAPI.Tests.Core
|
|||
public void ValidateManifests_ModStatus_AssumeBroken_Fails()
|
||||
{
|
||||
// arrange
|
||||
Mock<IModMetadata> mock = this.GetMetadata("Mod A", new string[0], allowStatusChange: true);
|
||||
this.SetupMetadataForValidation(mock, new ModDataRecordVersionedFields
|
||||
Mock<IModMetadata> mock = this.GetMetadata("Mod A", Array.Empty<string>(), allowStatusChange: true);
|
||||
mock.Setup(p => p.DataRecord).Returns(() => new ModDataRecordVersionedFields(this.GetModDataRecord())
|
||||
{
|
||||
Status = ModStatus.AssumeBroken
|
||||
});
|
||||
|
||||
// act
|
||||
new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: key => null);
|
||||
new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null, validateFilesExist: false);
|
||||
|
||||
// assert
|
||||
mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny<ModFailReason>(), It.IsAny<string>(), It.IsAny<string>()), Times.Once, "The validation did not fail the metadata.");
|
||||
|
@ -161,12 +170,11 @@ namespace SMAPI.Tests.Core
|
|||
public void ValidateManifests_MinimumApiVersion_Fails()
|
||||
{
|
||||
// arrange
|
||||
Mock<IModMetadata> mock = this.GetMetadata("Mod A", new string[0], allowStatusChange: true);
|
||||
Mock<IModMetadata> mock = this.GetMetadata("Mod A", Array.Empty<string>(), allowStatusChange: true);
|
||||
mock.Setup(p => p.Manifest).Returns(this.GetManifest(minimumApiVersion: "1.1"));
|
||||
this.SetupMetadataForValidation(mock);
|
||||
|
||||
// act
|
||||
new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: key => null);
|
||||
new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null, validateFilesExist: false);
|
||||
|
||||
// assert
|
||||
mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny<ModFailReason>(), It.IsAny<string>(), It.IsAny<string>()), Times.Once, "The validation did not fail the metadata.");
|
||||
|
@ -176,32 +184,33 @@ namespace SMAPI.Tests.Core
|
|||
public void ValidateManifests_MissingEntryDLL_Fails()
|
||||
{
|
||||
// arrange
|
||||
Mock<IModMetadata> mock = this.GetMetadata(this.GetManifest(id: "Mod A", version: "1.0", entryDll: "Missing.dll"), allowStatusChange: true);
|
||||
this.SetupMetadataForValidation(mock);
|
||||
string directoryPath = this.GetTempFolderPath();
|
||||
Mock<IModMetadata> mock = this.GetMetadata(this.GetManifest(id: "Mod A", version: "1.0", entryDll: "Missing.dll"), allowStatusChange: true, directoryPath: directoryPath);
|
||||
Directory.CreateDirectory(directoryPath);
|
||||
|
||||
// act
|
||||
new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: key => null);
|
||||
new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null);
|
||||
|
||||
// assert
|
||||
mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny<ModFailReason>(), It.IsAny<string>(), It.IsAny<string>()), Times.Once, "The validation did not fail the metadata.");
|
||||
|
||||
// cleanup
|
||||
Directory.Delete(directoryPath);
|
||||
}
|
||||
|
||||
[Test(Description = "Assert that validation fails when multiple mods have the same unique ID.")]
|
||||
public void ValidateManifests_DuplicateUniqueID_Fails()
|
||||
{
|
||||
// arrange
|
||||
Mock<IModMetadata> modA = this.GetMetadata("Mod A", new string[0], allowStatusChange: true);
|
||||
Mock<IModMetadata> modA = this.GetMetadata("Mod A", Array.Empty<string>(), allowStatusChange: true);
|
||||
Mock<IModMetadata> modB = this.GetMetadata(this.GetManifest(id: "Mod A", name: "Mod B", version: "1.0"), allowStatusChange: true);
|
||||
Mock<IModMetadata> modC = this.GetMetadata("Mod C", new string[0], allowStatusChange: false);
|
||||
foreach (Mock<IModMetadata> mod in new[] { modA, modB, modC })
|
||||
this.SetupMetadataForValidation(mod);
|
||||
|
||||
// act
|
||||
new ModResolver().ValidateManifests(new[] { modA.Object, modB.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: key => null);
|
||||
new ModResolver().ValidateManifests(new[] { modA.Object, modB.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null, validateFilesExist: false);
|
||||
|
||||
// assert
|
||||
modA.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny<ModFailReason>(), It.IsAny<string>(), It.IsAny<string>()), Times.Once, "The validation did not fail the first mod with a unique ID.");
|
||||
modB.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny<ModFailReason>(), It.IsAny<string>(), It.IsAny<string>()), Times.Once, "The validation did not fail the second mod with a unique ID.");
|
||||
modA.Verify(p => p.SetStatus(ModMetadataStatus.Failed, ModFailReason.Duplicate, It.IsAny<string>(), It.IsAny<string>()), Times.AtLeastOnce, "The validation did not fail the first mod with a unique ID.");
|
||||
modB.Verify(p => p.SetStatus(ModMetadataStatus.Failed, ModFailReason.Duplicate, It.IsAny<string>(), It.IsAny<string>()), Times.AtLeastOnce, "The validation did not fail the second mod with a unique ID.");
|
||||
}
|
||||
|
||||
[Test(Description = "Assert that validation fails when the manifest references a DLL that does not exist.")]
|
||||
|
@ -213,20 +222,23 @@ namespace SMAPI.Tests.Core
|
|||
// create DLL
|
||||
string modFolder = Path.Combine(this.GetTempFolderPath(), Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(modFolder);
|
||||
File.WriteAllText(Path.Combine(modFolder, manifest.EntryDll), "");
|
||||
File.WriteAllText(Path.Combine(modFolder, manifest.EntryDll!), "");
|
||||
|
||||
// arrange
|
||||
Mock<IModMetadata> mock = new Mock<IModMetadata>(MockBehavior.Strict);
|
||||
Mock<IModMetadata> mock = new(MockBehavior.Strict);
|
||||
mock.Setup(p => p.Status).Returns(ModMetadataStatus.Found);
|
||||
mock.Setup(p => p.DataRecord).Returns(() => null);
|
||||
mock.Setup(p => p.DataRecord).Returns(this.GetModDataRecordVersionedFields());
|
||||
mock.Setup(p => p.Manifest).Returns(manifest);
|
||||
mock.Setup(p => p.DirectoryPath).Returns(modFolder);
|
||||
|
||||
// act
|
||||
new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: key => null);
|
||||
new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null);
|
||||
|
||||
// assert
|
||||
// if Moq doesn't throw a method-not-setup exception, the validation didn't override the status.
|
||||
|
||||
// cleanup
|
||||
Directory.Delete(modFolder, recursive: true);
|
||||
}
|
||||
|
||||
/****
|
||||
|
@ -236,7 +248,7 @@ namespace SMAPI.Tests.Core
|
|||
public void ProcessDependencies_NoMods_DoesNothing()
|
||||
{
|
||||
// act
|
||||
IModMetadata[] mods = new ModResolver().ProcessDependencies(new IModMetadata[0], new ModDatabase()).ToArray();
|
||||
IModMetadata[] mods = new ModResolver().ProcessDependencies(Array.Empty<IModMetadata>(), new ModDatabase()).ToArray();
|
||||
|
||||
// assert
|
||||
Assert.AreEqual(0, mods.Length, 0, "Expected to get an empty list of mods.");
|
||||
|
@ -265,7 +277,7 @@ namespace SMAPI.Tests.Core
|
|||
public void ProcessDependencies_Skips_Failed()
|
||||
{
|
||||
// arrange
|
||||
Mock<IModMetadata> mock = new Mock<IModMetadata>(MockBehavior.Strict);
|
||||
Mock<IModMetadata> mock = new(MockBehavior.Strict);
|
||||
mock.Setup(p => p.Status).Returns(ModMetadataStatus.Failed);
|
||||
|
||||
// act
|
||||
|
@ -380,7 +392,7 @@ namespace SMAPI.Tests.Core
|
|||
Mock<IModMetadata> modA = this.GetMetadata("Mod A");
|
||||
Mock<IModMetadata> modB = this.GetMetadata("Mod B", dependencies: new[] { "Mod A" });
|
||||
Mock<IModMetadata> modC = this.GetMetadata("Mod C", dependencies: new[] { "Mod B" }, allowStatusChange: true);
|
||||
Mock<IModMetadata> modD = new Mock<IModMetadata>(MockBehavior.Strict);
|
||||
Mock<IModMetadata> modD = new(MockBehavior.Strict);
|
||||
modD.Setup(p => p.Manifest).Returns<IManifest>(null);
|
||||
modD.Setup(p => p.Status).Returns(ModMetadataStatus.Failed);
|
||||
|
||||
|
@ -478,21 +490,20 @@ namespace SMAPI.Tests.Core
|
|||
/// <param name="contentPackForID">The <see cref="IManifest.ContentPackFor"/> value.</param>
|
||||
/// <param name="minimumApiVersion">The <see cref="IManifest.MinimumApiVersion"/> value.</param>
|
||||
/// <param name="dependencies">The <see cref="IManifest.Dependencies"/> value.</param>
|
||||
private Manifest GetManifest(string id = null, string name = null, string version = null, string entryDll = null, string contentPackForID = null, string minimumApiVersion = null, IManifestDependency[] dependencies = null)
|
||||
private Manifest GetManifest(string? id = null, string? name = null, string? version = null, string? entryDll = null, string? contentPackForID = null, string? minimumApiVersion = null, IManifestDependency[]? dependencies = null)
|
||||
{
|
||||
return new Manifest
|
||||
{
|
||||
UniqueID = id ?? $"{Sample.String()}.{Sample.String()}",
|
||||
Name = name ?? id ?? Sample.String(),
|
||||
Author = Sample.String(),
|
||||
Description = Sample.String(),
|
||||
Version = version != null ? new SemanticVersion(version) : new SemanticVersion(Sample.Int(), Sample.Int(), Sample.Int(), Sample.String()),
|
||||
EntryDll = entryDll ?? $"{Sample.String()}.dll",
|
||||
ContentPackFor = contentPackForID != null ? new ManifestContentPackFor { UniqueID = contentPackForID } : null,
|
||||
MinimumApiVersion = minimumApiVersion != null ? new SemanticVersion(minimumApiVersion) : null,
|
||||
Dependencies = dependencies ?? new IManifestDependency[0],
|
||||
UpdateKeys = new string[0]
|
||||
};
|
||||
return new Manifest(
|
||||
uniqueId: id ?? $"{Sample.String()}.{Sample.String()}",
|
||||
name: name ?? id ?? Sample.String(),
|
||||
author: Sample.String(),
|
||||
description: Sample.String(),
|
||||
version: version != null ? new SemanticVersion(version) : new SemanticVersion(Sample.Int(), Sample.Int(), Sample.Int(), Sample.String()),
|
||||
entryDll: entryDll ?? $"{Sample.String()}.dll",
|
||||
contentPackFor: contentPackForID != null ? new ManifestContentPackFor(contentPackForID, null) : null,
|
||||
minimumApiVersion: minimumApiVersion != null ? new SemanticVersion(minimumApiVersion) : null,
|
||||
dependencies: dependencies ?? Array.Empty<IManifestDependency>(),
|
||||
updateKeys: Array.Empty<string>()
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>Get a randomized basic manifest.</summary>
|
||||
|
@ -508,21 +519,27 @@ namespace SMAPI.Tests.Core
|
|||
/// <param name="allowStatusChange">Whether the code being tested is allowed to change the mod status.</param>
|
||||
private Mock<IModMetadata> GetMetadata(string uniqueID, string[] dependencies, bool allowStatusChange = false)
|
||||
{
|
||||
IManifest manifest = this.GetManifest(id: uniqueID, version: "1.0", dependencies: dependencies?.Select(dependencyID => (IManifestDependency)new ManifestDependency(dependencyID, null)).ToArray());
|
||||
IManifest manifest = this.GetManifest(id: uniqueID, version: "1.0", dependencies: dependencies.Select(dependencyID => (IManifestDependency)new ManifestDependency(dependencyID, null as ISemanticVersion)).ToArray());
|
||||
return this.GetMetadata(manifest, allowStatusChange);
|
||||
}
|
||||
|
||||
/// <summary>Get a randomized basic manifest.</summary>
|
||||
/// <param name="manifest">The mod manifest.</param>
|
||||
/// <param name="allowStatusChange">Whether the code being tested is allowed to change the mod status.</param>
|
||||
private Mock<IModMetadata> GetMetadata(IManifest manifest, bool allowStatusChange = false)
|
||||
/// <param name="directoryPath">The directory path the mod metadata should be pointed at, or <c>null</c> to generate a fake path.</param>
|
||||
private Mock<IModMetadata> GetMetadata(IManifest manifest, bool allowStatusChange = false, string? directoryPath = null)
|
||||
{
|
||||
Mock<IModMetadata> mod = new Mock<IModMetadata>(MockBehavior.Strict);
|
||||
mod.Setup(p => p.DataRecord).Returns(() => null);
|
||||
directoryPath ??= this.GetTempFolderPath();
|
||||
|
||||
Mock<IModMetadata> mod = new(MockBehavior.Strict);
|
||||
mod.Setup(p => p.DataRecord).Returns(this.GetModDataRecordVersionedFields());
|
||||
mod.Setup(p => p.Status).Returns(ModMetadataStatus.Found);
|
||||
mod.Setup(p => p.DisplayName).Returns(manifest.UniqueID);
|
||||
mod.Setup(p => p.DirectoryPath).Returns(directoryPath);
|
||||
mod.Setup(p => p.Manifest).Returns(manifest);
|
||||
mod.Setup(p => p.HasID(It.IsAny<string>())).Returns((string id) => manifest.UniqueID == id);
|
||||
mod.Setup(p => p.GetUpdateKeys(It.IsAny<bool>())).Returns(Enumerable.Empty<UpdateKey>());
|
||||
mod.Setup(p => p.GetRelativePathWithRoot()).Returns(directoryPath);
|
||||
if (allowStatusChange)
|
||||
{
|
||||
mod
|
||||
|
@ -533,17 +550,16 @@ namespace SMAPI.Tests.Core
|
|||
return mod;
|
||||
}
|
||||
|
||||
/// <summary>Set up a mock mod metadata for <see cref="ModResolver.ValidateManifests"/>.</summary>
|
||||
/// <param name="mod">The mock mod metadata.</param>
|
||||
/// <param name="modRecord">The extra metadata about the mod from SMAPI's internal data (if any).</param>
|
||||
private void SetupMetadataForValidation(Mock<IModMetadata> mod, ModDataRecordVersionedFields modRecord = null)
|
||||
/// <summary>Generate a default mod data record.</summary>
|
||||
private ModDataRecord GetModDataRecord()
|
||||
{
|
||||
mod.Setup(p => p.Status).Returns(ModMetadataStatus.Found);
|
||||
mod.Setup(p => p.DataRecord).Returns(() => null);
|
||||
mod.Setup(p => p.Manifest).Returns(this.GetManifest());
|
||||
mod.Setup(p => p.DirectoryPath).Returns(Path.GetTempPath());
|
||||
mod.Setup(p => p.DataRecord).Returns(modRecord);
|
||||
mod.Setup(p => p.GetUpdateKeys(It.IsAny<bool>())).Returns(Enumerable.Empty<UpdateKey>());
|
||||
return new("Default Display Name", new ModDataModel("Sample ID", null, ModWarning.None));
|
||||
}
|
||||
|
||||
/// <summary>Generate a default mod data versioned fields instance.</summary>
|
||||
private ModDataRecordVersionedFields GetModDataRecordVersionedFields()
|
||||
{
|
||||
return new ModDataRecordVersionedFields(this.GetModDataRecord());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using StardewModdingAPI;
|
||||
using StardewModdingAPI.Framework;
|
||||
using StardewModdingAPI.Framework.ModHelpers;
|
||||
using StardewModdingAPI.Framework.ModLoading;
|
||||
using StardewModdingAPI.Toolkit.Serialization.Models;
|
||||
using StardewValley;
|
||||
|
||||
namespace SMAPI.Tests.Core
|
||||
|
@ -16,7 +21,7 @@ namespace SMAPI.Tests.Core
|
|||
** Data
|
||||
*********/
|
||||
/// <summary>Sample translation text for unit tests.</summary>
|
||||
public static string[] Samples = { null, "", " ", "boop", " boop " };
|
||||
public static string?[] Samples = { null, "", " ", "boop", " boop " };
|
||||
|
||||
|
||||
/*********
|
||||
|
@ -32,15 +37,15 @@ namespace SMAPI.Tests.Core
|
|||
var data = new Dictionary<string, IDictionary<string, string>>();
|
||||
|
||||
// act
|
||||
ITranslationHelper helper = new TranslationHelper("ModID", "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data);
|
||||
ITranslationHelper helper = new TranslationHelper(this.CreateModMetadata(), "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data);
|
||||
Translation translation = helper.Get("key");
|
||||
Translation[] translationList = helper.GetTranslations()?.ToArray();
|
||||
Translation[]? translationList = helper.GetTranslations()?.ToArray();
|
||||
|
||||
// assert
|
||||
Assert.AreEqual("en", helper.Locale, "The locale doesn't match the input value.");
|
||||
Assert.AreEqual(LocalizedContentManager.LanguageCode.en, helper.LocaleEnum, "The locale enum doesn't match the input value.");
|
||||
Assert.IsNotNull(translationList, "The full list of translations is unexpectedly null.");
|
||||
Assert.AreEqual(0, translationList.Length, "The full list of translations is unexpectedly not empty.");
|
||||
Assert.AreEqual(0, translationList!.Length, "The full list of translations is unexpectedly not empty.");
|
||||
|
||||
Assert.IsNotNull(translation, "The translation helper unexpectedly returned a null translation.");
|
||||
Assert.AreEqual(this.GetPlaceholderText("key"), translation.ToString(), "The translation returned an unexpected value.");
|
||||
|
@ -54,8 +59,8 @@ namespace SMAPI.Tests.Core
|
|||
var expected = this.GetExpectedTranslations();
|
||||
|
||||
// act
|
||||
var actual = new Dictionary<string, Translation[]>();
|
||||
TranslationHelper helper = new TranslationHelper("ModID", "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data);
|
||||
var actual = new Dictionary<string, Translation[]?>();
|
||||
TranslationHelper helper = new TranslationHelper(this.CreateModMetadata(), "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data);
|
||||
foreach (string locale in expected.Keys)
|
||||
{
|
||||
this.AssertSetLocale(helper, locale, LocalizedContentManager.LanguageCode.en);
|
||||
|
@ -79,7 +84,7 @@ namespace SMAPI.Tests.Core
|
|||
|
||||
// act
|
||||
var actual = new Dictionary<string, Translation[]>();
|
||||
TranslationHelper helper = new TranslationHelper("ModID", "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data);
|
||||
TranslationHelper helper = new TranslationHelper(this.CreateModMetadata(), "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data);
|
||||
foreach (string locale in expected.Keys)
|
||||
{
|
||||
this.AssertSetLocale(helper, locale, LocalizedContentManager.LanguageCode.en);
|
||||
|
@ -107,16 +112,16 @@ namespace SMAPI.Tests.Core
|
|||
[TestCase(" ", ExpectedResult = true)]
|
||||
[TestCase("boop", ExpectedResult = true)]
|
||||
[TestCase(" boop ", ExpectedResult = true)]
|
||||
public bool Translation_HasValue(string text)
|
||||
public bool Translation_HasValue(string? text)
|
||||
{
|
||||
return new Translation("pt-BR", "key", text).HasValue();
|
||||
}
|
||||
|
||||
[Test(Description = "Assert that the translation's ToString method returns the expected text for various inputs.")]
|
||||
public void Translation_ToString([ValueSource(nameof(TranslationTests.Samples))] string text)
|
||||
public void Translation_ToString([ValueSource(nameof(TranslationTests.Samples))] string? text)
|
||||
{
|
||||
// act
|
||||
Translation translation = new Translation("pt-BR", "key", text);
|
||||
Translation translation = new("pt-BR", "key", text);
|
||||
|
||||
// assert
|
||||
if (translation.HasValue())
|
||||
|
@ -126,20 +131,20 @@ namespace SMAPI.Tests.Core
|
|||
}
|
||||
|
||||
[Test(Description = "Assert that the translation's implicit string conversion returns the expected text for various inputs.")]
|
||||
public void Translation_ImplicitStringConversion([ValueSource(nameof(TranslationTests.Samples))] string text)
|
||||
public void Translation_ImplicitStringConversion([ValueSource(nameof(TranslationTests.Samples))] string? text)
|
||||
{
|
||||
// act
|
||||
Translation translation = new Translation("pt-BR", "key", text);
|
||||
Translation translation = new("pt-BR", "key", text);
|
||||
|
||||
// assert
|
||||
if (translation.HasValue())
|
||||
Assert.AreEqual(text, (string)translation, "The translation returned an unexpected value given a valid input.");
|
||||
Assert.AreEqual(text, (string?)translation, "The translation returned an unexpected value given a valid input.");
|
||||
else
|
||||
Assert.AreEqual(this.GetPlaceholderText("key"), (string)translation, "The translation returned an unexpected value given a null or empty input.");
|
||||
Assert.AreEqual(this.GetPlaceholderText("key"), (string?)translation, "The translation returned an unexpected value given a null or empty input.");
|
||||
}
|
||||
|
||||
[Test(Description = "Assert that the translation returns the expected text given a use-placeholder setting.")]
|
||||
public void Translation_UsePlaceholder([Values(true, false)] bool value, [ValueSource(nameof(TranslationTests.Samples))] string text)
|
||||
public void Translation_UsePlaceholder([Values(true, false)] bool value, [ValueSource(nameof(TranslationTests.Samples))] string? text)
|
||||
{
|
||||
// act
|
||||
Translation translation = new Translation("pt-BR", "key", text).UsePlaceholder(value);
|
||||
|
@ -154,7 +159,7 @@ namespace SMAPI.Tests.Core
|
|||
}
|
||||
|
||||
[Test(Description = "Assert that the translation returns the expected text after setting the default.")]
|
||||
public void Translation_Default([ValueSource(nameof(TranslationTests.Samples))] string text, [ValueSource(nameof(TranslationTests.Samples))] string @default)
|
||||
public void Translation_Default([ValueSource(nameof(TranslationTests.Samples))] string? text, [ValueSource(nameof(TranslationTests.Samples))] string? @default)
|
||||
{
|
||||
// act
|
||||
Translation translation = new Translation("pt-BR", "key", text).Default(@default);
|
||||
|
@ -182,7 +187,7 @@ namespace SMAPI.Tests.Core
|
|||
string expected = $"{start} tokens are properly replaced (including {middle} {middle}) {end}";
|
||||
|
||||
// act
|
||||
Translation translation = new Translation("pt-BR", "key", input);
|
||||
Translation translation = new("pt-BR", "key", input);
|
||||
switch (structure)
|
||||
{
|
||||
case "anonymous object":
|
||||
|
@ -190,7 +195,7 @@ namespace SMAPI.Tests.Core
|
|||
break;
|
||||
|
||||
case "class":
|
||||
translation = translation.Tokens(new TokenModel { Start = start, Middle = middle, End = end });
|
||||
translation = translation.Tokens(new TokenModel(start, middle, end));
|
||||
break;
|
||||
|
||||
case "IDictionary<string, object>":
|
||||
|
@ -324,21 +329,63 @@ namespace SMAPI.Tests.Core
|
|||
return string.Format(Translation.PlaceholderText, key);
|
||||
}
|
||||
|
||||
/// <summary>Create a fake mod manifest.</summary>
|
||||
private IModMetadata CreateModMetadata()
|
||||
{
|
||||
string id = $"smapi.unit-tests.fake-mod-{Guid.NewGuid():N}";
|
||||
|
||||
string tempPath = Path.Combine(Path.GetTempPath(), id);
|
||||
return new ModMetadata(
|
||||
displayName: "Mod Display Name",
|
||||
directoryPath: tempPath,
|
||||
rootPath: tempPath,
|
||||
manifest: new Manifest(
|
||||
uniqueID: id,
|
||||
name: "Mod Name",
|
||||
author: "Mod Author",
|
||||
description: "Mod Description",
|
||||
version: new SemanticVersion(1, 0, 0)
|
||||
),
|
||||
dataRecord: null,
|
||||
isIgnored: false
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/*********
|
||||
** Test models
|
||||
*********/
|
||||
/// <summary>A model used to test token support.</summary>
|
||||
[SuppressMessage("ReSharper", "NotAccessedField.Local", Justification = "Used dynamically via translation helper.")]
|
||||
[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local", Justification = "Used dynamically via translation helper.")]
|
||||
private class TokenModel
|
||||
{
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>A sample token property.</summary>
|
||||
public string Start { get; set; }
|
||||
public string Start { get; }
|
||||
|
||||
/// <summary>A sample token property.</summary>
|
||||
public string Middle { get; set; }
|
||||
public string Middle { get; }
|
||||
|
||||
/// <summary>A sample token field.</summary>
|
||||
public string End;
|
||||
|
||||
|
||||
/*********
|
||||
** public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="start">A sample token property.</param>
|
||||
/// <param name="middle">A sample token field.</param>
|
||||
/// <param name="end">A sample token property.</param>
|
||||
public TokenModel(string start, string middle, string end)
|
||||
{
|
||||
this.Start = start;
|
||||
this.Middle = middle;
|
||||
this.End = end;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,21 +1,20 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<AssemblyName>SMAPI.Tests</AssemblyName>
|
||||
<RootNamespace>SMAPI.Tests</RootNamespace>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<LangVersion>latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="..\..\build\common.targets" />
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SMAPI.Tests.ModApiConsumer\SMAPI.Tests.ModApiConsumer.csproj" />
|
||||
<ProjectReference Include="..\SMAPI.Tests.ModApiProvider\SMAPI.Tests.ModApiProvider.csproj" />
|
||||
<ProjectReference Include="..\SMAPI.Toolkit.CoreInterfaces\SMAPI.Toolkit.CoreInterfaces.csproj" />
|
||||
<ProjectReference Include="..\SMAPI.Toolkit\SMAPI.Toolkit.csproj" />
|
||||
<ProjectReference Include="..\SMAPI\SMAPI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.5.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
|
||||
<PackageReference Include="Moq" Version="4.16.1" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
|
|
|
@ -9,7 +9,7 @@ namespace SMAPI.Tests
|
|||
** Fields
|
||||
*********/
|
||||
/// <summary>A random number generator.</summary>
|
||||
private static readonly Random Random = new Random();
|
||||
private static readonly Random Random = new();
|
||||
|
||||
|
||||
/*********
|
||||
|
|
|
@ -21,12 +21,12 @@ namespace SMAPI.Tests.Utilities
|
|||
public void TryParse_SimpleValue(SButton button)
|
||||
{
|
||||
// act
|
||||
bool success = KeybindList.TryParse($"{button}", out KeybindList parsed, out string[] errors);
|
||||
bool success = KeybindList.TryParse($"{button}", out KeybindList? parsed, out string[] errors);
|
||||
|
||||
// assert
|
||||
Assert.IsTrue(success, "Parsing unexpectedly failed.");
|
||||
Assert.IsNotNull(parsed, "The parsed result should not be null.");
|
||||
Assert.AreEqual(parsed.ToString(), $"{button}");
|
||||
Assert.AreEqual(parsed!.ToString(), $"{button}");
|
||||
Assert.IsNotNull(errors, message: "The errors should never be null.");
|
||||
Assert.IsEmpty(errors, message: "The input bindings incorrectly reported errors.");
|
||||
}
|
||||
|
@ -44,17 +44,17 @@ namespace SMAPI.Tests.Utilities
|
|||
[TestCase(",", ExpectedResult = "None")]
|
||||
[TestCase("A,", ExpectedResult = "A")]
|
||||
[TestCase(",A", ExpectedResult = "A")]
|
||||
public string TryParse_MultiValues(string input)
|
||||
public string TryParse_MultiValues(string? input)
|
||||
{
|
||||
// act
|
||||
bool success = KeybindList.TryParse(input, out KeybindList parsed, out string[] errors);
|
||||
bool success = KeybindList.TryParse(input, out KeybindList? parsed, out string[] errors);
|
||||
|
||||
// assert
|
||||
Assert.IsTrue(success, "Parsing unexpectedly failed.");
|
||||
Assert.IsNotNull(parsed, "The parsed result should not be null.");
|
||||
Assert.IsNotNull(errors, message: "The errors should never be null.");
|
||||
Assert.IsEmpty(errors, message: "The input bindings incorrectly reported errors.");
|
||||
return parsed.ToString();
|
||||
return parsed!.ToString();
|
||||
}
|
||||
|
||||
/// <summary>Assert invalid values are rejected.</summary>
|
||||
|
@ -67,7 +67,7 @@ namespace SMAPI.Tests.Utilities
|
|||
public void TryParse_InvalidValues(string input, string expectedError)
|
||||
{
|
||||
// act
|
||||
bool success = KeybindList.TryParse(input, out KeybindList parsed, out string[] errors);
|
||||
bool success = KeybindList.TryParse(input, out KeybindList? parsed, out string[] errors);
|
||||
|
||||
// assert
|
||||
Assert.IsFalse(success, "Parsing unexpectedly succeeded.");
|
||||
|
@ -98,13 +98,15 @@ namespace SMAPI.Tests.Utilities
|
|||
public SButtonState GetState(string input, string stateMap)
|
||||
{
|
||||
// act
|
||||
bool success = KeybindList.TryParse(input, out KeybindList parsed, out string[] errors);
|
||||
bool success = KeybindList.TryParse(input, out KeybindList? parsed, out string[] errors);
|
||||
if (success && parsed?.Keybinds != null)
|
||||
{
|
||||
foreach (var keybind in parsed.Keybinds)
|
||||
foreach (Keybind? keybind in parsed.Keybinds)
|
||||
{
|
||||
#pragma warning disable 618 // method is marked obsolete because it should only be used in unit tests
|
||||
keybind.GetButtonState = key => this.GetStateFromMap(key, stateMap);
|
||||
#pragma warning restore 618
|
||||
}
|
||||
}
|
||||
|
||||
// assert
|
||||
|
@ -112,7 +114,7 @@ namespace SMAPI.Tests.Utilities
|
|||
Assert.IsNotNull(parsed, "The parsed result should not be null.");
|
||||
Assert.IsNotNull(errors, message: "The errors should never be null.");
|
||||
Assert.IsEmpty(errors, message: "The input bindings incorrectly reported errors.");
|
||||
return parsed.GetState();
|
||||
return parsed!.GetState();
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using NUnit.Framework;
|
||||
using StardewModdingAPI.Toolkit.Utilities;
|
||||
|
@ -6,6 +7,7 @@ namespace SMAPI.Tests.Utilities
|
|||
{
|
||||
/// <summary>Unit tests for <see cref="PathUtilities"/>.</summary>
|
||||
[TestFixture]
|
||||
[SuppressMessage("ReSharper", "StringLiteralTypo", Justification = "These are standard game install paths.")]
|
||||
internal class PathUtilitiesTests
|
||||
{
|
||||
/*********
|
||||
|
@ -14,136 +16,125 @@ namespace SMAPI.Tests.Utilities
|
|||
/// <summary>Sample paths used in unit tests.</summary>
|
||||
public static readonly SamplePath[] SamplePaths = {
|
||||
// Windows absolute path
|
||||
new SamplePath
|
||||
{
|
||||
OriginalPath = @"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley",
|
||||
new(
|
||||
OriginalPath: @"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley",
|
||||
|
||||
Segments = new[] { "C:", "Program Files (x86)", "Steam", "steamapps", "common", "Stardew Valley" },
|
||||
SegmentsLimit3 = new [] { "C:", "Program Files (x86)", @"Steam\steamapps\common\Stardew Valley" },
|
||||
Segments: new[] { "C:", "Program Files (x86)", "Steam", "steamapps", "common", "Stardew Valley" },
|
||||
SegmentsLimit3: new [] { "C:", "Program Files (x86)", @"Steam\steamapps\common\Stardew Valley" },
|
||||
|
||||
NormalizedOnWindows = @"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley",
|
||||
NormalizedOnUnix = @"C:/Program Files (x86)/Steam/steamapps/common/Stardew Valley"
|
||||
},
|
||||
NormalizedOnWindows: @"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley",
|
||||
NormalizedOnUnix: @"C:/Program Files (x86)/Steam/steamapps/common/Stardew Valley"
|
||||
),
|
||||
|
||||
// Windows absolute path (with trailing slash)
|
||||
new SamplePath
|
||||
{
|
||||
OriginalPath = @"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley\",
|
||||
new(
|
||||
OriginalPath: @"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley\",
|
||||
|
||||
Segments = new[] { "C:", "Program Files (x86)", "Steam", "steamapps", "common", "Stardew Valley" },
|
||||
SegmentsLimit3 = new [] { "C:", "Program Files (x86)", @"Steam\steamapps\common\Stardew Valley\" },
|
||||
Segments: new[] { "C:", "Program Files (x86)", "Steam", "steamapps", "common", "Stardew Valley" },
|
||||
SegmentsLimit3: new [] { "C:", "Program Files (x86)", @"Steam\steamapps\common\Stardew Valley\" },
|
||||
|
||||
NormalizedOnWindows = @"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley\",
|
||||
NormalizedOnUnix = @"C:/Program Files (x86)/Steam/steamapps/common/Stardew Valley/"
|
||||
},
|
||||
NormalizedOnWindows: @"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley\",
|
||||
NormalizedOnUnix: @"C:/Program Files (x86)/Steam/steamapps/common/Stardew Valley/"
|
||||
),
|
||||
|
||||
// Windows relative path
|
||||
new SamplePath
|
||||
{
|
||||
OriginalPath = @"Content\Characters\Dialogue\Abigail",
|
||||
new(
|
||||
OriginalPath: @"Content\Characters\Dialogue\Abigail",
|
||||
|
||||
Segments = new [] { "Content", "Characters", "Dialogue", "Abigail" },
|
||||
SegmentsLimit3 = new [] { "Content", "Characters", @"Dialogue\Abigail" },
|
||||
Segments: new [] { "Content", "Characters", "Dialogue", "Abigail" },
|
||||
SegmentsLimit3: new [] { "Content", "Characters", @"Dialogue\Abigail" },
|
||||
|
||||
NormalizedOnWindows = @"Content\Characters\Dialogue\Abigail",
|
||||
NormalizedOnUnix = @"Content/Characters/Dialogue/Abigail"
|
||||
},
|
||||
NormalizedOnWindows: @"Content\Characters\Dialogue\Abigail",
|
||||
NormalizedOnUnix: @"Content/Characters/Dialogue/Abigail"
|
||||
),
|
||||
|
||||
// Windows relative path (with directory climbing)
|
||||
new SamplePath
|
||||
{
|
||||
OriginalPath = @"..\..\Content",
|
||||
new(
|
||||
OriginalPath: @"..\..\Content",
|
||||
|
||||
Segments = new [] { "..", "..", "Content" },
|
||||
SegmentsLimit3 = new [] { "..", "..", "Content" },
|
||||
Segments: new [] { "..", "..", "Content" },
|
||||
SegmentsLimit3: new [] { "..", "..", "Content" },
|
||||
|
||||
NormalizedOnWindows = @"..\..\Content",
|
||||
NormalizedOnUnix = @"../../Content"
|
||||
},
|
||||
NormalizedOnWindows: @"..\..\Content",
|
||||
NormalizedOnUnix: @"../../Content"
|
||||
),
|
||||
|
||||
// Windows UNC path
|
||||
new SamplePath
|
||||
{
|
||||
OriginalPath = @"\\unc\path",
|
||||
new(
|
||||
OriginalPath: @"\\unc\path",
|
||||
|
||||
Segments = new [] { "unc", "path" },
|
||||
SegmentsLimit3 = new [] { "unc", "path" },
|
||||
Segments: new [] { "unc", "path" },
|
||||
SegmentsLimit3: new [] { "unc", "path" },
|
||||
|
||||
NormalizedOnWindows = @"\\unc\path",
|
||||
NormalizedOnUnix = "/unc/path" // there's no good way to normalize this on Unix since UNC paths aren't supported; path normalization is meant for asset names anyway, so this test only ensures it returns some sort of sane value
|
||||
},
|
||||
NormalizedOnWindows: @"\\unc\path",
|
||||
NormalizedOnUnix: "/unc/path" // there's no good way to normalize this on Unix since UNC paths aren't supported; path normalization is meant for asset names anyway, so this test only ensures it returns some sort of sane value
|
||||
),
|
||||
|
||||
// Linux absolute path
|
||||
new SamplePath
|
||||
{
|
||||
OriginalPath = @"/home/.steam/steam/steamapps/common/Stardew Valley",
|
||||
new(
|
||||
OriginalPath: @"/home/.steam/steam/steamapps/common/Stardew Valley",
|
||||
|
||||
Segments = new [] { "home", ".steam", "steam", "steamapps", "common", "Stardew Valley" },
|
||||
SegmentsLimit3 = new [] { "home", ".steam", "steam/steamapps/common/Stardew Valley" },
|
||||
Segments: new [] { "home", ".steam", "steam", "steamapps", "common", "Stardew Valley" },
|
||||
SegmentsLimit3: new [] { "home", ".steam", "steam/steamapps/common/Stardew Valley" },
|
||||
|
||||
NormalizedOnWindows = @"\home\.steam\steam\steamapps\common\Stardew Valley",
|
||||
NormalizedOnUnix = @"/home/.steam/steam/steamapps/common/Stardew Valley"
|
||||
},
|
||||
NormalizedOnWindows: @"\home\.steam\steam\steamapps\common\Stardew Valley",
|
||||
NormalizedOnUnix: @"/home/.steam/steam/steamapps/common/Stardew Valley"
|
||||
),
|
||||
|
||||
// Linux absolute path (with trailing slash)
|
||||
new SamplePath
|
||||
{
|
||||
OriginalPath = @"/home/.steam/steam/steamapps/common/Stardew Valley/",
|
||||
new(
|
||||
OriginalPath: @"/home/.steam/steam/steamapps/common/Stardew Valley/",
|
||||
|
||||
Segments = new [] { "home", ".steam", "steam", "steamapps", "common", "Stardew Valley" },
|
||||
SegmentsLimit3 = new [] { "home", ".steam", "steam/steamapps/common/Stardew Valley/" },
|
||||
Segments: new [] { "home", ".steam", "steam", "steamapps", "common", "Stardew Valley" },
|
||||
SegmentsLimit3: new [] { "home", ".steam", "steam/steamapps/common/Stardew Valley/" },
|
||||
|
||||
NormalizedOnWindows = @"\home\.steam\steam\steamapps\common\Stardew Valley\",
|
||||
NormalizedOnUnix = @"/home/.steam/steam/steamapps/common/Stardew Valley/"
|
||||
},
|
||||
NormalizedOnWindows: @"\home\.steam\steam\steamapps\common\Stardew Valley\",
|
||||
NormalizedOnUnix: @"/home/.steam/steam/steamapps/common/Stardew Valley/"
|
||||
),
|
||||
|
||||
// Linux absolute path (with ~)
|
||||
new SamplePath
|
||||
{
|
||||
OriginalPath = @"~/.steam/steam/steamapps/common/Stardew Valley",
|
||||
new(
|
||||
OriginalPath: @"~/.steam/steam/steamapps/common/Stardew Valley",
|
||||
|
||||
Segments = new [] { "~", ".steam", "steam", "steamapps", "common", "Stardew Valley" },
|
||||
SegmentsLimit3 = new [] { "~", ".steam", "steam/steamapps/common/Stardew Valley" },
|
||||
Segments: new [] { "~", ".steam", "steam", "steamapps", "common", "Stardew Valley" },
|
||||
SegmentsLimit3: new [] { "~", ".steam", "steam/steamapps/common/Stardew Valley" },
|
||||
|
||||
NormalizedOnWindows = @"~\.steam\steam\steamapps\common\Stardew Valley",
|
||||
NormalizedOnUnix = @"~/.steam/steam/steamapps/common/Stardew Valley"
|
||||
},
|
||||
NormalizedOnWindows: @"~\.steam\steam\steamapps\common\Stardew Valley",
|
||||
NormalizedOnUnix: @"~/.steam/steam/steamapps/common/Stardew Valley"
|
||||
),
|
||||
|
||||
// Linux relative path
|
||||
new SamplePath
|
||||
{
|
||||
OriginalPath = @"Content/Characters/Dialogue/Abigail",
|
||||
new(
|
||||
OriginalPath: @"Content/Characters/Dialogue/Abigail",
|
||||
|
||||
Segments = new [] { "Content", "Characters", "Dialogue", "Abigail" },
|
||||
SegmentsLimit3 = new [] { "Content", "Characters", "Dialogue/Abigail" },
|
||||
Segments: new [] { "Content", "Characters", "Dialogue", "Abigail" },
|
||||
SegmentsLimit3: new [] { "Content", "Characters", "Dialogue/Abigail" },
|
||||
|
||||
NormalizedOnWindows = @"Content\Characters\Dialogue\Abigail",
|
||||
NormalizedOnUnix = @"Content/Characters/Dialogue/Abigail"
|
||||
},
|
||||
NormalizedOnWindows: @"Content\Characters\Dialogue\Abigail",
|
||||
NormalizedOnUnix: @"Content/Characters/Dialogue/Abigail"
|
||||
),
|
||||
|
||||
// Linux relative path (with directory climbing)
|
||||
new SamplePath
|
||||
{
|
||||
OriginalPath = @"../../Content",
|
||||
new(
|
||||
OriginalPath: @"../../Content",
|
||||
|
||||
Segments = new [] { "..", "..", "Content" },
|
||||
SegmentsLimit3 = new [] { "..", "..", "Content" },
|
||||
Segments: new [] { "..", "..", "Content" },
|
||||
SegmentsLimit3: new [] { "..", "..", "Content" },
|
||||
|
||||
NormalizedOnWindows = @"..\..\Content",
|
||||
NormalizedOnUnix = @"../../Content"
|
||||
},
|
||||
NormalizedOnWindows: @"..\..\Content",
|
||||
NormalizedOnUnix: @"../../Content"
|
||||
),
|
||||
|
||||
// Mixed directory separators
|
||||
new SamplePath
|
||||
{
|
||||
OriginalPath = @"C:\some/mixed\path/separators",
|
||||
new(
|
||||
OriginalPath: @"C:\some/mixed\path/separators",
|
||||
|
||||
Segments = new [] { "C:", "some", "mixed", "path", "separators" },
|
||||
SegmentsLimit3 = new [] { "C:", "some", @"mixed\path/separators" },
|
||||
Segments: new [] { "C:", "some", "mixed", "path", "separators" },
|
||||
SegmentsLimit3: new [] { "C:", "some", @"mixed\path/separators" },
|
||||
|
||||
NormalizedOnWindows = @"C:\some\mixed\path\separators",
|
||||
NormalizedOnUnix = @"C:/some/mixed/path/separators"
|
||||
},
|
||||
NormalizedOnWindows: @"C:\some\mixed\path\separators",
|
||||
NormalizedOnUnix: @"C:/some/mixed/path/separators"
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
|
@ -281,14 +272,14 @@ namespace SMAPI.Tests.Utilities
|
|||
/*********
|
||||
** Private classes
|
||||
*********/
|
||||
public class SamplePath
|
||||
/// <summary>A sample path in multiple formats.</summary>
|
||||
/// <param name="OriginalPath">The original path to pass to the <see cref="PathUtilities"/>.</param>
|
||||
/// <param name="Segments">The normalized path segments.</param>
|
||||
/// <param name="SegmentsLimit3">The normalized path segments, if we stop segmenting after the second one.</param>
|
||||
/// <param name="NormalizedOnWindows">The normalized form on Windows.</param>
|
||||
/// <param name="NormalizedOnUnix">The normalized form on Linux or macOS.</param>
|
||||
public record SamplePath(string OriginalPath, string[] Segments, string[] SegmentsLimit3, string NormalizedOnWindows, string NormalizedOnUnix)
|
||||
{
|
||||
public string OriginalPath { get; set; }
|
||||
public string[] Segments { get; set; }
|
||||
public string[] SegmentsLimit3 { get; set; }
|
||||
public string NormalizedOnWindows { get; set; }
|
||||
public string NormalizedOnUnix { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return this.OriginalPath;
|
||||
|
|
|
@ -16,9 +16,12 @@ namespace SMAPI.Tests.Utilities
|
|||
/*********
|
||||
** Fields
|
||||
*********/
|
||||
/// <summary>All valid seasons.</summary>
|
||||
/// <summary>The valid seasons.</summary>
|
||||
private static readonly string[] ValidSeasons = { "spring", "summer", "fall", "winter" };
|
||||
|
||||
/// <summary>Sample user inputs for season names.</summary>
|
||||
private static readonly string[] SampleSeasonValues = SDateTests.ValidSeasons.Concat(new[] { " WIntEr " }).ToArray();
|
||||
|
||||
/// <summary>All valid days of a month.</summary>
|
||||
private static readonly int[] ValidDays = Enumerable.Range(1, 28).ToArray();
|
||||
|
||||
|
@ -55,19 +58,18 @@ namespace SMAPI.Tests.Utilities
|
|||
** Constructor
|
||||
****/
|
||||
[Test(Description = "Assert that the constructor sets the expected values for all valid dates.")]
|
||||
public void Constructor_SetsExpectedValues([ValueSource(nameof(SDateTests.ValidSeasons))] string season, [ValueSource(nameof(SDateTests.ValidDays))] int day, [Values(1, 2, 100)] int year)
|
||||
public void Constructor_SetsExpectedValues([ValueSource(nameof(SDateTests.SampleSeasonValues))] string season, [ValueSource(nameof(SDateTests.ValidDays))] int day, [Values(1, 2, 100)] int year)
|
||||
{
|
||||
// act
|
||||
SDate date = new SDate(day, season, year);
|
||||
SDate date = new(day, season, year);
|
||||
|
||||
// assert
|
||||
Assert.AreEqual(day, date.Day);
|
||||
Assert.AreEqual(season, date.Season);
|
||||
Assert.AreEqual(season.Trim().ToLowerInvariant(), date.Season);
|
||||
Assert.AreEqual(year, date.Year);
|
||||
}
|
||||
|
||||
[Test(Description = "Assert that the constructor throws an exception if the values are invalid.")]
|
||||
[TestCase(01, "Spring", 1)] // seasons are case-sensitive
|
||||
[TestCase(01, "springs", 1)] // invalid season name
|
||||
[TestCase(-1, "spring", 1)] // day < 0
|
||||
[TestCase(0, "spring", 1)] // day zero
|
||||
|
@ -252,9 +254,9 @@ namespace SMAPI.Tests.Utilities
|
|||
{
|
||||
foreach (int day in SDateTests.ValidDays)
|
||||
{
|
||||
SDate date = new SDate(day, season, year);
|
||||
SDate date = new(day, season, year);
|
||||
int hash = date.GetHashCode();
|
||||
if (hashes.TryGetValue(hash, out SDate otherDate))
|
||||
if (hashes.TryGetValue(hash, out SDate? otherDate))
|
||||
Assert.Fail($"Received identical hash code {hash} for dates {otherDate} and {date}.");
|
||||
if (hash < lastHash)
|
||||
Assert.Fail($"Received smaller hash code for date {date} ({hash}) relative to {hashes[lastHash]} ({lastHash}).");
|
||||
|
@ -294,7 +296,7 @@ namespace SMAPI.Tests.Utilities
|
|||
[TestCase(Dates.Now, Dates.NextDay, ExpectedResult = false)]
|
||||
[TestCase(Dates.Now, Dates.NextMonth, ExpectedResult = false)]
|
||||
[TestCase(Dates.Now, Dates.NextYear, ExpectedResult = false)]
|
||||
public bool Operators_Equals(string now, string other)
|
||||
public bool Operators_Equals(string? now, string other)
|
||||
{
|
||||
return this.GetDate(now) == this.GetDate(other);
|
||||
}
|
||||
|
@ -308,7 +310,7 @@ namespace SMAPI.Tests.Utilities
|
|||
[TestCase(Dates.Now, Dates.NextDay, ExpectedResult = true)]
|
||||
[TestCase(Dates.Now, Dates.NextMonth, ExpectedResult = true)]
|
||||
[TestCase(Dates.Now, Dates.NextYear, ExpectedResult = true)]
|
||||
public bool Operators_NotEquals(string now, string other)
|
||||
public bool Operators_NotEquals(string? now, string other)
|
||||
{
|
||||
return this.GetDate(now) != this.GetDate(other);
|
||||
}
|
||||
|
@ -322,7 +324,7 @@ namespace SMAPI.Tests.Utilities
|
|||
[TestCase(Dates.Now, Dates.NextDay, ExpectedResult = true)]
|
||||
[TestCase(Dates.Now, Dates.NextMonth, ExpectedResult = true)]
|
||||
[TestCase(Dates.Now, Dates.NextYear, ExpectedResult = true)]
|
||||
public bool Operators_LessThan(string now, string other)
|
||||
public bool Operators_LessThan(string? now, string other)
|
||||
{
|
||||
return this.GetDate(now) < this.GetDate(other);
|
||||
}
|
||||
|
@ -336,7 +338,7 @@ namespace SMAPI.Tests.Utilities
|
|||
[TestCase(Dates.Now, Dates.NextDay, ExpectedResult = true)]
|
||||
[TestCase(Dates.Now, Dates.NextMonth, ExpectedResult = true)]
|
||||
[TestCase(Dates.Now, Dates.NextYear, ExpectedResult = true)]
|
||||
public bool Operators_LessThanOrEqual(string now, string other)
|
||||
public bool Operators_LessThanOrEqual(string? now, string other)
|
||||
{
|
||||
return this.GetDate(now) <= this.GetDate(other);
|
||||
}
|
||||
|
@ -350,7 +352,7 @@ namespace SMAPI.Tests.Utilities
|
|||
[TestCase(Dates.Now, Dates.NextDay, ExpectedResult = false)]
|
||||
[TestCase(Dates.Now, Dates.NextMonth, ExpectedResult = false)]
|
||||
[TestCase(Dates.Now, Dates.NextYear, ExpectedResult = false)]
|
||||
public bool Operators_MoreThan(string now, string other)
|
||||
public bool Operators_MoreThan(string? now, string other)
|
||||
{
|
||||
return this.GetDate(now) > this.GetDate(other);
|
||||
}
|
||||
|
@ -364,7 +366,7 @@ namespace SMAPI.Tests.Utilities
|
|||
[TestCase(Dates.Now, Dates.NextDay, ExpectedResult = false)]
|
||||
[TestCase(Dates.Now, Dates.NextMonth, ExpectedResult = false)]
|
||||
[TestCase(Dates.Now, Dates.NextYear, ExpectedResult = false)]
|
||||
public bool Operators_MoreThanOrEqual(string now, string other)
|
||||
public bool Operators_MoreThanOrEqual(string? now, string other)
|
||||
{
|
||||
return this.GetDate(now) > this.GetDate(other);
|
||||
}
|
||||
|
@ -375,7 +377,8 @@ namespace SMAPI.Tests.Utilities
|
|||
*********/
|
||||
/// <summary>Convert a string date into a game date, to make unit tests easier to read.</summary>
|
||||
/// <param name="dateStr">The date string like "dd MMMM yy".</param>
|
||||
private SDate GetDate(string dateStr)
|
||||
[return: NotNullIfNotNull("dateStr")]
|
||||
private SDate? GetDate(string? dateStr)
|
||||
{
|
||||
if (dateStr == null)
|
||||
return null;
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue