mirror of
https://github.com/Pathoschild/SMAPI.git
synced 2026-10-12 10:39:54 +00:00
Merge branch 'develop' into stable
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
|
||||
<!--set properties -->
|
||||
<PropertyGroup>
|
||||
<Version>3.6.2</Version>
|
||||
<Version>3.7.0</Version>
|
||||
<Product>SMAPI</Product>
|
||||
|
||||
<LangVersion>latest</LangVersion>
|
||||
|
||||
+40
-7
@@ -7,12 +7,45 @@
|
||||
* Migrated to Harmony 2.0 (see [_migrate to Harmony 2.0_](https://stardewvalleywiki.com/Modding:Migrate_to_Harmony_2.0) for more info).
|
||||
-->
|
||||
|
||||
## 3.7
|
||||
Released 07 September 2020 for Stardew Valley 1.4.1 or later. See [release highlights](https://www.patreon.com/posts/41341767).
|
||||
|
||||
* For players:
|
||||
* Added heuristic compatibility rewrites. (This improves mod compatibility with Android and future game updates.)
|
||||
* Tweaked the rules for showing update alerts (see _for SMAPI developers_ below for details).
|
||||
* Simplified the error shown for duplicate mods.
|
||||
* Fixed crossplatform compatibility for mods which use the `[HarmonyPatch(type)]` attribute (thanks to spacechase0!).
|
||||
* Fixed map tile rotation broken when you return to the title screen and reload a save.
|
||||
* Fixed broken URL in update alerts for unofficial versions.
|
||||
* Fixed rare error when a mod adds/removes event handlers asynchronously.
|
||||
* Fixed rare issue where the console showed incorrect colors when mods wrote to it asynchronously.
|
||||
* Fixed SMAPI not always detecting broken field references in mod code.
|
||||
* Removed the experimental `RewriteInParallel` option added in SMAPI 3.6 (it was already disabled by default). Unfortunately this caused intermittent and unpredictable errors when enabled.
|
||||
* Internal changes to prepare for upcoming game updates.
|
||||
|
||||
* For modders:
|
||||
* Added `PathUtilities` to simplify working with file/asset names.
|
||||
* You can now read/write `SDate` values to JSON (e.g. for `config.json`, network mod messages, etc).
|
||||
* Fixed asset propagation not updating title menu buttons immediately on Linux/Mac.
|
||||
|
||||
* For the web UI:
|
||||
* Updated the JSON validator/schema for Content Patcher 1.16 and 1.17.
|
||||
|
||||
* For SMAPI developers:
|
||||
* The web API now returns an update alert in two new cases: any newer unofficial update (previously only shown if the mod was incompatible), and a newer prerelease version if the installed non-prerelease version is broken (previously only shown if the installed version was prerelease).
|
||||
* Reorganised the SMAPI core to reduce coupling to game types like `Game1`, make it easier to navigate, and simplify future game updates.
|
||||
* SMAPI now automatically fixes code broken by these changes in game code, so manual rewriters are no longer needed:
|
||||
* reference to a method with new optional parameters;
|
||||
* reference to a field replaced by a property;
|
||||
* reference to a field replaced by a `const` field.
|
||||
* `FieldReplaceRewriter` now supports mapping to a different target type.
|
||||
|
||||
## 3.6.2
|
||||
Released 02 August 2020 for Stardew Valley 1.4.1 or later.
|
||||
|
||||
* For players:
|
||||
* Improved compatibility with some Linux terminals (thanks to jlaw and Spatterjaaay!).
|
||||
* Fixed rare crash when a mod adds/removes an event handler from an event handler.
|
||||
* Fixed rare error when a mod adds/removes an event handler from an event handler.
|
||||
* Fixed string sorting/comparison for some special characters.
|
||||
|
||||
* For the Console Commands mod:
|
||||
@@ -31,7 +64,7 @@ Released 21 June 2020 for Stardew Valley 1.4.1 or later.
|
||||
* Fixed event priority sorting.
|
||||
|
||||
## 3.6
|
||||
Released 20 June 2020 for Stardew Valley 1.4.1 or later.
|
||||
Released 20 June 2020 for Stardew Valley 1.4.1 or later. See [release highlights](https://www.patreon.com/posts/38441800).
|
||||
|
||||
* For players:
|
||||
* Added crossplatform compatibility for mods which use the `[HarmonyPatch(type)]` attribute.
|
||||
@@ -73,7 +106,7 @@ Released 20 June 2020 for Stardew Valley 1.4.1 or later.
|
||||
* Changed SMAPI's Harmony ID from `io.smapi` to `SMAPI` for readability in Harmony summaries.
|
||||
|
||||
## 3.5
|
||||
Released 27 April 2020 for Stardew Valley 1.4.1 or later.
|
||||
Released 27 April 2020 for Stardew Valley 1.4.1 or later. See [release highlights](https://www.patreon.com/posts/36471055).
|
||||
|
||||
* For players:
|
||||
* SMAPI now prevents more game errors due to broken items, so you no longer need save editing to remove them.
|
||||
@@ -107,7 +140,7 @@ Released 24 March 2020 for Stardew Valley 1.4.1 or later.
|
||||
* Fixed mouse input suppression not working in SMAPI 3.4.
|
||||
|
||||
## 3.4
|
||||
Released 22 March 2020 for Stardew Valley 1.4.1 or later.
|
||||
Released 22 March 2020 for Stardew Valley 1.4.1 or later. See [release highlights](https://www.patreon.com/posts/35161371).
|
||||
|
||||
* For players:
|
||||
* Fixed semi-transparency issues on Linux/Mac in recent versions of Mono (e.g. pink shadows).
|
||||
@@ -140,7 +173,7 @@ Released 22 February 2020 for Stardew Valley 1.4.1 or later.
|
||||
* Fixed errors with custom spouse room mods in SMAPI 3.3.
|
||||
|
||||
## 3.3
|
||||
Released 22 February 2020 for Stardew Valley 1.4.1 or later.
|
||||
Released 22 February 2020 for Stardew Valley 1.4.1 or later. See [release highlights](https://www.patreon.com/posts/34248719).
|
||||
|
||||
* For players:
|
||||
* Improved performance for mods which load many images.
|
||||
@@ -174,7 +207,7 @@ Released 22 February 2020 for Stardew Valley 1.4.1 or later.
|
||||
* The SMAPI log now prefixes the OS name with `Android` on Android.
|
||||
|
||||
## 3.2
|
||||
Released 01 February 2020 for Stardew Valley 1.4.1 or later.
|
||||
Released 01 February 2020 for Stardew Valley 1.4.1 or later. See [release highlights](https://www.patreon.com/posts/33659728).
|
||||
|
||||
* For players:
|
||||
* SMAPI now prevents crashes due to invalid schedule data.
|
||||
@@ -215,7 +248,7 @@ Released 01 February 2020 for Stardew Valley 1.4.1 or later.
|
||||
* Dropped API support for the pre-3.0 update-check format.
|
||||
|
||||
## 3.1
|
||||
Released 05 January 2019 for Stardew Valley 1.4.1 or later.
|
||||
Released 05 January 2019 for Stardew Valley 1.4.1 or later. See [release highlights](https://www.patreon.com/posts/32904041).
|
||||
|
||||
* For players:
|
||||
* Added separate group in 'skipped mods' list for broken dependencies, so it's easier to see what to fix first.
|
||||
|
||||
+274
-156
@@ -6,16 +6,12 @@ for SMAPI mods and related tools. The package is fully compatible with Linux, Ma
|
||||
## Contents
|
||||
* [Use](#use)
|
||||
* [Features](#features)
|
||||
* [Detect game path](#detect-game-path)
|
||||
* [Add assembly references](#add-assembly-references)
|
||||
* [Copy files into the `Mods` folder and create release zip](#copy-files-into-the-mods-folder-and-create-release-zip)
|
||||
* [Launch or debug game](#launch-or-debug-game)
|
||||
* [Preconfigure common settings](#preconfigure-common-settings)
|
||||
* [Add code warnings](#add-code-warnings)
|
||||
* [Configure](#configure)
|
||||
* [Code warnings](#code-warnings)
|
||||
* [Special cases](#special-cases)
|
||||
* [Custom game path](#custom-game-path)
|
||||
* [Non-mod projects](#non-mod-projects)
|
||||
* [FAQs](#faqs)
|
||||
* [How do I set the game path?](#custom-game-path)
|
||||
* [How do I change which files are included in the mod deploy/zip?](#how-do-i-change-which-files-are-included-in-the-mod-deployzip)
|
||||
* [Can I use the package for non-mod projects?](#can-i-use-the-package-for-non-mod-projects)
|
||||
* [For SMAPI developers](#for-smapi-developers)
|
||||
* [Release notes](#release-notes)
|
||||
|
||||
@@ -27,133 +23,226 @@ for SMAPI mods and related tools. The package is fully compatible with Linux, Ma
|
||||
5. Run the game to play with your mod.
|
||||
|
||||
## Features
|
||||
The package automatically makes the changes listed below. In some cases you can configure how it
|
||||
works by editing your mod's `.csproj` file, and adding the given properties between the first
|
||||
`<PropertyGroup>` and `</PropertyGroup>` tags.
|
||||
The package includes several features to simplify mod development (see [_configure_](#configure) to
|
||||
change how these work):
|
||||
|
||||
### Detect game path
|
||||
The package finds your game folder by scanning the default install paths and Windows registry. It
|
||||
adds two MSBuild properties for use in your `.csproj` file if needed:
|
||||
* **Detect game path:**
|
||||
The package automatically finds your game folder by scanning the default install paths and
|
||||
Windows registry. It adds two MSBuild properties for use in your `.csproj` file if needed:
|
||||
`$(GamePath)` and `$(GameExecutableName)`.
|
||||
|
||||
property | description
|
||||
-------- | -----------
|
||||
`$(GamePath)` | The absolute path to the detected game folder.
|
||||
`$(GameExecutableName)` | The game's executable name for the current OS (`Stardew Valley` on Windows, or `StardewValley` on Linux/Mac).
|
||||
* **Add assembly references:**
|
||||
The package adds assembly references to SMAPI, Stardew Valley, xTile, and the game framework
|
||||
(MonoGame on Linux/Mac, XNA Framework on Windows). It automatically adjusts depending on which OS
|
||||
you're compiling it on. If you use [Harmony](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Harmony),
|
||||
it can optionally add a reference to that too.
|
||||
|
||||
If you get a build error saying it can't find your game, see [_custom game path_](#custom-game-path).
|
||||
* **Copy files into the `Mods` folder:**
|
||||
The package automatically copies your mod's DLL and PDB files, `manifest.json`, [`i18n`
|
||||
files](https://stardewvalleywiki.com/Modding:Translations) (if any), the `assets` folder (if
|
||||
any), and [build output](https://stackoverflow.com/a/10828462/262123) into your game's `Mods`
|
||||
folder when you rebuild the code, with a subfolder matching the mod's project name. That lets you
|
||||
try the mod in-game right after building it.
|
||||
|
||||
### Add assembly references
|
||||
The package adds assembly references to SMAPI, Stardew Valley, xTile, and MonoGame (Linux/Mac) or XNA
|
||||
Framework (Windows). It automatically adjusts depending on which OS you're compiling it on.
|
||||
* **Create release zip:**
|
||||
The package adds a zip file in your project's `bin` folder when you rebuild the code, in the
|
||||
format recommended for uploading to mod sites like Nexus Mods. This includes the same files as
|
||||
the previous feature.
|
||||
|
||||
The assemblies aren't copied to the build output, since mods loaded by SMAPI won't need them. For
|
||||
non-mod projects like unit tests, you can set this property:
|
||||
* **Launch or debug mod:**
|
||||
On Windows only, the package configures Visual Studio so you can launch the game and attach a
|
||||
debugger using _Debug > Start Debugging_ or _Debug > Start Without Debugging_. This lets you [set
|
||||
breakpoints](https://docs.microsoft.com/en-us/visualstudio/debugger/using-breakpoints?view=vs-2019)
|
||||
in your code while the game is running, or [make simple changes to the mod code without needing to
|
||||
restart the game](https://docs.microsoft.com/en-us/visualstudio/debugger/edit-and-continue?view=vs-2019).
|
||||
This is disabled on Linux/Mac due to limitations with the Mono wrapper.
|
||||
|
||||
* **Preconfigure common settings:**
|
||||
The package automatically enables `.pdb` files (so error logs show line numbers to simplify
|
||||
debugging), and enables support for the simplified SDK-style `.csproj` format.
|
||||
|
||||
* **Add code warnings:**
|
||||
The package runs code analysis on your mod and raises warnings for some common errors or
|
||||
pitfalls. See [_code warnings_](#code-warnings) for more info.
|
||||
|
||||
## Configure
|
||||
### How to set options
|
||||
You can configure the package by setting build properties, which are essentially tags like this:
|
||||
```xml
|
||||
<CopyModReferencesToBuildOutput>true</CopyModReferencesToBuildOutput>
|
||||
<PropertyGroup>
|
||||
<ModFolderName>CustomModName</ModFolderName>
|
||||
<EnableModDeploy>false</EnableModDeploy>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
If your mod uses [Harmony](https://github.com/pardeike/Harmony) (not recommended for most mods),
|
||||
the package can add a reference to SMAPI's Harmony DLL for you:
|
||||
```xml
|
||||
<EnableHarmony>true</EnableHarmony>
|
||||
```
|
||||
There are two places you can put them:
|
||||
|
||||
### Copy files into the `Mods` folder and create release zip
|
||||
<dl>
|
||||
<dt>Files considered part of your mod</dt>
|
||||
<dd>
|
||||
* **Global properties** apply to every mod project you open on your computer. That's recommended
|
||||
for properties you want to set for all mods (e.g. a custom game path). Here's where to put them:
|
||||
|
||||
These files are selected by default: `manifest.json`,
|
||||
[`i18n` files](https://stardewvalleywiki.com/Modding:Translations) (if any), the `assets` folder
|
||||
(if any), and all files in the build output. You can select custom files by [adding them to the
|
||||
build output](https://stackoverflow.com/a/10828462/262123). (If your project references another mod,
|
||||
make sure the reference is [_not_ marked 'copy local'](https://msdn.microsoft.com/en-us/library/t1zz5y8c(v=vs.100).aspx).)
|
||||
1. Open the home folder on your computer (see instructions for
|
||||
[Linux](https://superuser.com/questions/409218/where-is-my-users-home-folder-in-ubuntu),
|
||||
[MacOS](https://www.cnet.com/how-to/how-to-find-your-macs-home-folder-and-add-it-to-finder/),
|
||||
or [Windows](https://www.computerhope.com/issues/ch000109.htm)).
|
||||
2. Create a `stardewvalley.targets` file with this content:
|
||||
```xml
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
```
|
||||
3. Add the properties between the `<PropertyGroup>` and `</PropertyGroup>`.
|
||||
|
||||
You can deselect a file by removing it from the build output. For a default file, you can set the
|
||||
property below to a comma-delimited list of regex patterns to ignore. For crossplatform
|
||||
compatibility, you should replace path delimiters with `[/\\]`.
|
||||
* **Project properties** apply to a specific project. This is mainly useful for mod-specific
|
||||
options like the mod name. Here's where to put them:
|
||||
|
||||
1. Open the folder containing your mod's source code.
|
||||
2. Open the `.csproj` file in a text editor (Notepad is fine).
|
||||
3. Add the properties between the first `<PropertyGroup>` and `</PropertyGroup>` tags you find.
|
||||
|
||||
### Available properties
|
||||
These are the options you can set:
|
||||
|
||||
<ul>
|
||||
<li>Game properties:
|
||||
<table>
|
||||
<tr>
|
||||
<th>property</th>
|
||||
<th>effect</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>GamePath</code></td>
|
||||
<td>
|
||||
|
||||
The absolute path to the Stardew Valley folder. This is auto-detected, so you usually don't need to
|
||||
change it.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>GameModsPath</code></td>
|
||||
<td>
|
||||
|
||||
The absolute path to the folder containing the game's installed mods (defaults to
|
||||
`$(GamePath)/Mods`), used when deploying the mod files.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>GameExecutableName</code></td>
|
||||
<td>
|
||||
|
||||
The filename for the game's executable (i.e. `StardewValley.exe` on Linux/Mac or
|
||||
`Stardew Valley.exe` on Windows). This is auto-detected, and you should almost never change this.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</li>
|
||||
|
||||
<li>Mod build properties:
|
||||
<table>
|
||||
<tr>
|
||||
<th>property</th>
|
||||
<th>effect</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>EnableHarmony</code></td>
|
||||
<td>
|
||||
|
||||
Whether to add a reference to [Harmony](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Harmony)
|
||||
(default `false`). This is only needed if you use Harmony.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>EnableModDeploy</code></td>
|
||||
<td>
|
||||
|
||||
Whether to copy the mod files into your game's `Mods` folder (default `true`).
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>EnableModZip</code></td>
|
||||
<td>
|
||||
|
||||
Whether to create a release-ready `.zip` file in the mod project's `bin` folder (default `true`).
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>ModFolderName</code></td>
|
||||
<td>
|
||||
|
||||
The mod name for its folder under `Mods` and its release zip (defaults to the project name).
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>ModZipPath</code></td>
|
||||
<td>
|
||||
|
||||
The folder path where the release zip is created (defaults to the project's `bin` folder).
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</li>
|
||||
|
||||
<li>Specialized properties:
|
||||
<table>
|
||||
<tr>
|
||||
<th>property</th>
|
||||
<th>effect</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>CopyModReferencesToBuildOutput</code></td>
|
||||
<td>
|
||||
|
||||
Whether to copy game and framework DLLs into the mod folder (default `false`). This is useful for
|
||||
unit test projects, but not needed for mods that'll be run through SMAPI.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>EnableGameDebugging</code></td>
|
||||
<td>
|
||||
|
||||
Whether to configure the project so you can launch or debug the game through the _Debug_ menu in
|
||||
Visual Studio (default `true`). There's usually no reason to change this, unless it's a unit test
|
||||
project.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>IgnoreModFilePatterns</code></td>
|
||||
<td>
|
||||
|
||||
A comma-delimited list of regex patterns matching files to ignore when deploying or zipping the mod
|
||||
files (default empty). For crossplatform compatibility, you should replace path delimiters with `[/\\]`.
|
||||
|
||||
For example, this excludes all `.txt` and `.pdf` files, as well as the `assets/paths.png` file:
|
||||
|
||||
```xml
|
||||
<IgnoreModFilePatterns>\.txt$, \.pdf$, assets[/\\]paths.png</IgnoreModFilePatterns>
|
||||
```
|
||||
|
||||
</dd>
|
||||
<dt>Copy files into the `Mods` folder</dt>
|
||||
<dd>
|
||||
|
||||
The package copies the selected files into your game's `Mods` folder when you rebuild the code,
|
||||
with a subfolder matching the mod's project name.
|
||||
|
||||
You can change the folder name:
|
||||
```xml
|
||||
<ModFolderName>YourModName</ModFolderName>
|
||||
```
|
||||
|
||||
Or disable deploying the files:
|
||||
```xml
|
||||
<EnableModDeploy>false</EnableModDeploy>
|
||||
```
|
||||
|
||||
</dd>
|
||||
<dt>Create release zip</dt>
|
||||
<dd>
|
||||
|
||||
The package adds a zip file in your project's `bin` folder when you rebuild the code, in the format
|
||||
recommended for sites like Nexus Mods. The zip filename can be changed using `ModFolderName` above.
|
||||
|
||||
You can change the folder path where the zip is created:
|
||||
```xml
|
||||
<ModZipPath>$(SolutionDir)\_releases</ModZipPath>
|
||||
```
|
||||
|
||||
Or disable zip creation:
|
||||
```xml
|
||||
<EnableModZip>false</EnableModZip>
|
||||
```
|
||||
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
### Launch or debug game
|
||||
On Windows only, the package configures Visual Studio so you can launch the game and attach a
|
||||
debugger using _Debug > Start Debugging_ or _Debug > Start Without Debugging_. This lets you [set
|
||||
breakpoints](https://docs.microsoft.com/en-us/visualstudio/debugger/using-breakpoints?view=vs-2019)
|
||||
in your code while the game is running, or [make simple changes to the mod code without needing to
|
||||
restart the game](https://docs.microsoft.com/en-us/visualstudio/debugger/edit-and-continue?view=vs-2019).
|
||||
|
||||
This is disabled on Linux/Mac due to limitations with the Mono wrapper.
|
||||
|
||||
To disable game debugging (only needed for some non-mod projects):
|
||||
|
||||
```xml
|
||||
<EnableGameDebugging>false</EnableGameDebugging>
|
||||
```
|
||||
|
||||
### Preconfigure common settings
|
||||
The package also automatically enables PDB files (so error logs show line numbers for simpler
|
||||
debugging), and enables support for the simplified `.csproj` format.
|
||||
|
||||
### Add code warnings
|
||||
The package runs code analysis on your mod and raises warnings for some common errors or pitfalls.
|
||||
See [_code warnings_](#code-warnings) for more info.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
## Code warnings
|
||||
### Overview
|
||||
The NuGet package adds code warnings in Visual Studio specific to Stardew Valley. For example:
|
||||

|
||||
|
||||
You can hide the warnings using the warning ID (shown under 'code' in the Error List). See...
|
||||
* [for specific code](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/preprocessor-directives/preprocessor-pragma-warning);
|
||||
* for a method using this attribute:
|
||||
```cs
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("SMAPI.CommonErrors", "AvoidNetField")]
|
||||
```
|
||||
* for an entire project:
|
||||
1. Expand the _References_ node for the project in Visual Studio.
|
||||
2. Right-click on _Analyzers_ and choose _Open Active Rule Set_.
|
||||
4. Expand _StardewModdingAPI.ModBuildConfig.Analyzer_ and uncheck the warnings you want to hide.
|
||||
You can [hide the warnings](https://visualstudiomagazine.com/articles/2017/09/01/hide-compiler-warnings.aspx)
|
||||
if needed using the warning ID (shown under 'code' in the Error List).
|
||||
|
||||
See below for help with each specific warning.
|
||||
See below for help with specific warnings.
|
||||
|
||||
### Avoid implicit net field cast
|
||||
Warning text:
|
||||
@@ -202,57 +291,37 @@ Warning text:
|
||||
|
||||
Your code accesses a field which is obsolete or no longer works. Use the suggested field instead.
|
||||
|
||||
## Special cases
|
||||
### Custom game path
|
||||
The package usually detects where your game is installed automatically. If it can't find your game
|
||||
or you have multiple installs, you can specify the path yourself. There's two ways to do that:
|
||||
## FAQs
|
||||
### How do I set the game path?<span id="custom-game-path"></span>
|
||||
The package detects where your game is installed automatically, so you usually don't need to set it
|
||||
manually. If it can't find your game or you have multiple installs, you can specify the path
|
||||
yourself.
|
||||
|
||||
* **Option 1: global game path (recommended).**
|
||||
_This will apply to every project that uses the package._
|
||||
To do that:
|
||||
|
||||
1. Get the full folder path containing the Stardew Valley executable.
|
||||
2. Create this file:
|
||||
|
||||
platform | path
|
||||
--------- | ----
|
||||
Linux/Mac | `~/stardewvalley.targets`
|
||||
Windows | `%USERPROFILE%\stardewvalley.targets`
|
||||
|
||||
3. Save the file with this content:
|
||||
|
||||
```xml
|
||||
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<GamePath>PATH_HERE</GamePath>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
4. Replace `PATH_HERE` with your game's folder path.
|
||||
|
||||
* **Option 2: path in the project file.**
|
||||
_You'll need to do this for each project that uses the package._
|
||||
|
||||
1. Get the folder path containing the Stardew Valley `.exe` file.
|
||||
2. Add this to your `.csproj` file under the `<Project` line:
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
1. Get the full folder path containing the Stardew Valley executable.
|
||||
2. See [_configure_](#configure) to add this property:
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<GamePath>PATH_HERE</GamePath>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
3. Replace `PATH_HERE` with your custom game install path.
|
||||
</PropertyGroup>
|
||||
```
|
||||
3. Replace `PATH_HERE` with your game's folder path.
|
||||
|
||||
The configuration will check your custom path first, then fall back to the default paths (so it'll
|
||||
still compile on a different computer).
|
||||
|
||||
You access the game path via `$(GamePath)` in MSBuild properties, if you need to reference another
|
||||
file in the game folder.
|
||||
### How do I change which files are included in the mod deploy/zip?
|
||||
For custom files, you can [add/remove them in the build output](https://stackoverflow.com/a/10828462/262123).
|
||||
(If your project references another mod, make sure the reference is [_not_ marked 'copy
|
||||
local'](https://msdn.microsoft.com/en-us/library/t1zz5y8c(v=vs.100).aspx).)
|
||||
|
||||
### Non-mod projects
|
||||
To exclude a file the package copies by default, see `IgnoreModFilePatterns` under
|
||||
[_configure_](#configure).
|
||||
|
||||
### Can I use the package for non-mod projects?
|
||||
You can use the package in non-mod projects too (e.g. unit tests or framework DLLs). Just disable
|
||||
the mod-related package features:
|
||||
the mod-related package features (see [_configure_](#configure)):
|
||||
|
||||
```xml
|
||||
<EnableGameDebugging>false</EnableGameDebugging>
|
||||
@@ -278,7 +347,7 @@ To prepare a build of the NuGet package:
|
||||
1. Install the [NuGet CLI](https://docs.microsoft.com/en-us/nuget/install-nuget-client-tools#nugetexe-cli).
|
||||
1. Change the version and release notes in `package.nuspec`.
|
||||
2. Rebuild the solution in _Release_ mode.
|
||||
3. Open a terminal in the `bin/Pathoschild.Stardew.ModBuildConfig` package and run this command:
|
||||
3. Open a terminal in the `bin/Pathoschild.Stardew.ModBuildConfig` folder and run this command:
|
||||
```bash
|
||||
nuget.exe pack
|
||||
```
|
||||
@@ -287,11 +356,21 @@ That will create a `Pathoschild.Stardew.ModBuildConfig-<version>.nupkg` file in
|
||||
which can be uploaded to NuGet or referenced directly.
|
||||
|
||||
## Release notes
|
||||
### 3.2
|
||||
Released 07 September 2020.
|
||||
|
||||
* Added option to change `Mods` folder path.
|
||||
* Rewrote documentation to make it easier to read.
|
||||
|
||||
### 3.1
|
||||
Released 01 February 2020.
|
||||
|
||||
* Added support for semantic versioning 2.0.
|
||||
* `0Harmony.dll` is now ignored if the mod references Harmony directly (it's bundled with SMAPI).
|
||||
|
||||
### 3.0
|
||||
Released 26 November 2019.
|
||||
|
||||
* Updated for SMAPI 3.0 and Stardew Valley 1.4.
|
||||
* Added automatic support for `assets` folders.
|
||||
* Added `$(GameExecutableName)` MSBuild variable.
|
||||
@@ -305,11 +384,15 @@ which can be uploaded to NuGet or referenced directly.
|
||||
* Migrated package icon to NuGet's new format.
|
||||
|
||||
### 2.2
|
||||
Released 28 October 2018.
|
||||
|
||||
* Added support for SMAPI 2.8+ (still compatible with earlier versions).
|
||||
* Added default game paths for 32-bit Windows.
|
||||
* Fixed valid manifests marked invalid in some cases.
|
||||
|
||||
### 2.1
|
||||
Released 27 July 2018.
|
||||
|
||||
* Added support for Stardew Valley 1.3.
|
||||
* Added support for non-mod projects.
|
||||
* Added C# analyzers to warn about implicit conversions of Netcode fields in Stardew Valley 1.3.
|
||||
@@ -318,12 +401,18 @@ which can be uploaded to NuGet or referenced directly.
|
||||
* Fixed some game paths not detected by NuGet package.
|
||||
|
||||
### 2.0.2
|
||||
Released 01 November 2017.
|
||||
|
||||
* Fixed compatibility issue on Linux.
|
||||
|
||||
### 2.0.1
|
||||
Released 11 October 2017.
|
||||
|
||||
* Fixed mod deploy failing to create subfolders if they don't already exist.
|
||||
|
||||
### 2.0
|
||||
Released 11 October 2017.
|
||||
|
||||
* Added: mods are now copied into the `Mods` folder automatically (configurable).
|
||||
* Added: release zips are now created automatically in your build output folder (configurable).
|
||||
* Added: mod deploy and release zips now exclude Json.NET automatically, since it's provided by SMAPI.
|
||||
@@ -333,38 +422,67 @@ which can be uploaded to NuGet or referenced directly.
|
||||
* Fixed release zip failing if mod name contains characters that aren't valid in a filename.
|
||||
|
||||
### 1.7.1
|
||||
Released 28 July 2017.
|
||||
|
||||
* Fixed issue where i18n folders were flattened.
|
||||
* The manifest/i18n files in the project now take precedence over those in the build output if both
|
||||
are present.
|
||||
|
||||
### 1.7
|
||||
Released 28 July 2017.
|
||||
|
||||
* Added option to create release zips on build.
|
||||
* Added reference to XNA's XACT library for audio-related mods.
|
||||
|
||||
### 1.6.2
|
||||
Released 10 July 2017.
|
||||
|
||||
* Further improved crossplatform game path detection.
|
||||
* Removed undocumented `GamePlatform` build property.
|
||||
|
||||
### 1.6.1
|
||||
Released 09 July 2017.
|
||||
|
||||
* Improved crossplatform game path detection.
|
||||
|
||||
### 1.6
|
||||
Released 05 June 2017.
|
||||
|
||||
* Added support for deploying mod files into `Mods` automatically.
|
||||
* Added a build error if a game folder is found, but doesn't contain Stardew Valley or SMAPI.
|
||||
|
||||
### 1.5
|
||||
Released 23 January 2017.
|
||||
|
||||
* Added support for setting a custom game path globally.
|
||||
* Added default GOG path on Mac.
|
||||
|
||||
### 1.4
|
||||
Released 11 January 2017.
|
||||
|
||||
* Fixed detection of non-default game paths on 32-bit Windows.
|
||||
* Removed support for SilVerPLuM (discontinued).
|
||||
* Removed support for overriding the target platform (no longer needed since SMAPI crossplatforms
|
||||
mods automatically).
|
||||
|
||||
### 1.3
|
||||
Released 31 December 2016.
|
||||
|
||||
* Added support for non-default game paths on Windows.
|
||||
|
||||
### 1.2
|
||||
Released 24 October 2016.
|
||||
|
||||
* Exclude game binaries from mod build output.
|
||||
|
||||
### 1.1
|
||||
Released 21 October 2016.
|
||||
|
||||
* Added support for overriding the target platform.
|
||||
|
||||
### 1.0
|
||||
Released 21 October 2016.
|
||||
|
||||
* Initial release.
|
||||
* Added support for detecting the game path automatically.
|
||||
* Added support for injecting XNA/MonoGame references automatically based on the OS.
|
||||
|
||||
@@ -87,25 +87,24 @@ on the wiki for the first-time setup.
|
||||
build type | format | example
|
||||
:--------- | :----------------------- | :------
|
||||
dev build | `<version>-alpha.<date>` | `3.0.0-alpha.20171230`
|
||||
prerelease | `<version>-beta.<count>` | `3.0.0-beta.2`
|
||||
prerelease | `<version>-beta.<date>` | `3.0.0-beta.20171230`
|
||||
release | `<version>` | `3.0.0`
|
||||
|
||||
2. In Windows:
|
||||
1. Rebuild the solution in Release mode.
|
||||
2. Copy `windows-install.*` from `bin/SMAPI installer` and `bin/SMAPI installer for developers` to
|
||||
Linux/Mac.
|
||||
1. Rebuild the solution with the _release_ solution configuration.
|
||||
2. Copy `bin/SMAPI installer` and `bin/SMAPI installer for developers` to Linux/Mac.
|
||||
|
||||
3. In Linux/Mac:
|
||||
1. Rebuild the solution in Release mode.
|
||||
2. Add the `windows-install.*` files to the `bin/SMAPI installer` and
|
||||
`bin/SMAPI installer for developers` folders.
|
||||
1. Rebuild the solution with the _release_ solution configuration.
|
||||
2. Add the `windows-install.*` files from Windows to the `bin/SMAPI installer` and
|
||||
`bin/SMAPI installer for developers` folders compiled on Linux.
|
||||
3. Rename the folders to `SMAPI <version> installer` and `SMAPI <version> installer for developers`.
|
||||
4. Zip the two folders.
|
||||
|
||||
### Custom Harmony build
|
||||
SMAPI uses [a custom build of Harmony](https://github.com/Pathoschild/Harmony#readme), which is
|
||||
included in the `build` folder. To use a different build, just replace `0Harmony.dll` in that
|
||||
folder.
|
||||
folder before compiling.
|
||||
|
||||
## Release notes
|
||||
See [release notes](../release-notes.md).
|
||||
|
||||
@@ -9,4 +9,4 @@
|
||||
/// <summary>Remove SMAPI from the game directory.</summary>
|
||||
Uninstall
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,8 +35,6 @@ namespace StardewModdingApi.Installer
|
||||
"SMAPI.ConsoleCommands"
|
||||
};
|
||||
|
||||
|
||||
|
||||
/// <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>
|
||||
@@ -84,6 +82,7 @@ namespace StardewModdingApi.Installer
|
||||
foreach (DirectoryInfo modDir in modsDir.EnumerateDirectories())
|
||||
yield return Path.Combine(modDir.FullName, ".cache"); // 1.4–1.7
|
||||
}
|
||||
|
||||
yield return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "StardewValley", "ErrorLogs"); // remove old log files
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,6 @@ using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Reflection;
|
||||
using StardewModdingAPI.Internal;
|
||||
using StardewModdingAPI.Toolkit.Utilities;
|
||||
|
||||
namespace StardewModdingApi.Installer
|
||||
{
|
||||
|
||||
@@ -32,9 +32,9 @@ namespace StardewModdingAPI.ModBuildConfig
|
||||
[Required]
|
||||
public string TargetDir { get; set; }
|
||||
|
||||
/// <summary>The folder containing the game files.</summary>
|
||||
/// <summary>The folder containing the game's mod folders.</summary>
|
||||
[Required]
|
||||
public string GameDir { get; set; }
|
||||
public string GameModsDir { get; set; }
|
||||
|
||||
/// <summary>Whether to enable copying the mod files into the game's Mods folder.</summary>
|
||||
[Required]
|
||||
@@ -69,7 +69,7 @@ namespace StardewModdingAPI.ModBuildConfig
|
||||
// deploy mod files
|
||||
if (this.EnableModDeploy)
|
||||
{
|
||||
string outputPath = Path.Combine(this.GameDir, "Mods", this.EscapeInvalidFilenameCharacters(this.ModFolderName));
|
||||
string outputPath = Path.Combine(this.GameModsDir, this.EscapeInvalidFilenameCharacters(this.ModFolderName));
|
||||
this.Log.LogMessage(MessageImportance.High, $"The mod build package is copying the mod files to {outputPath}...");
|
||||
this.CreateModFolder(package.GetFiles(), outputPath);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<RootNamespace>StardewModdingAPI.ModBuildConfig</RootNamespace>
|
||||
<Version>3.1.0</Version>
|
||||
<Version>3.2.0</Version>
|
||||
<TargetFramework>net45</TargetFramework>
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
<!-- set default package options -->
|
||||
<ModFolderName Condition="'$(ModFolderName)' == ''">$(MSBuildProjectName)</ModFolderName>
|
||||
<ModZipPath Condition="'$(ModZipPath)' == ''">$(TargetDir)</ModZipPath>
|
||||
<GameModsPath Condition="'$(GameModsPath)' == ''">$([System.IO.Path]::Combine($(GamePath), 'Mods')</GameModsPath>
|
||||
<EnableModDeploy Condition="'$(EnableModDeploy)' == ''">true</EnableModDeploy>
|
||||
<EnableModZip Condition="'$(EnableModZip)' == ''">true</EnableModZip>
|
||||
<EnableHarmony Condition="'$(EnableHarmony)' == ''">false</EnableHarmony>
|
||||
@@ -86,7 +87,7 @@
|
||||
|
||||
ProjectDir="$(ProjectDir)"
|
||||
TargetDir="$(TargetDir)"
|
||||
GameDir="$(GamePath)"
|
||||
GameModsDir="$(GameModsPath)"
|
||||
IgnoreModFilePatterns="$(IgnoreModFilePatterns)"
|
||||
/>
|
||||
</Target>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
|
||||
<metadata>
|
||||
<id>Pathoschild.Stardew.ModBuildConfig</id>
|
||||
<version>3.1.0</version>
|
||||
<version>3.2.0</version>
|
||||
<title>Build package for SMAPI mods</title>
|
||||
<authors>Pathoschild</authors>
|
||||
<owners>Pathoschild</owners>
|
||||
@@ -14,9 +14,9 @@
|
||||
<iconUrl>https://raw.githubusercontent.com/Pathoschild/SMAPI/develop/src/SMAPI.ModBuildConfig/assets/nuget-icon.png</iconUrl>
|
||||
<description>Automates the build configuration for crossplatform Stardew Valley SMAPI mods. For SMAPI 3.0 or later.</description>
|
||||
<releaseNotes>
|
||||
3.1.0:
|
||||
- Added support for semantic versioning 2.0.
|
||||
- 0Harmony.dll is now ignored if the mod references it directly (it's bundled with SMAPI).
|
||||
3.2.0:
|
||||
- Added option to change `Mods` folder path.
|
||||
- Rewrote documentation to make it easier to read.
|
||||
</releaseNotes>
|
||||
</metadata>
|
||||
<files>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"Name": "Console Commands",
|
||||
"Author": "SMAPI",
|
||||
"Version": "3.6.2",
|
||||
"Version": "3.7.0",
|
||||
"Description": "Adds SMAPI console commands that let you manipulate the game.",
|
||||
"UniqueID": "SMAPI.ConsoleCommands",
|
||||
"EntryDll": "ConsoleCommands.dll",
|
||||
"MinimumApiVersion": "3.6.2"
|
||||
"MinimumApiVersion": "3.7.0"
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"Name": "Save Backup",
|
||||
"Author": "SMAPI",
|
||||
"Version": "3.6.2",
|
||||
"Version": "3.7.0",
|
||||
"Description": "Automatically backs up all your saves once per day into its folder.",
|
||||
"UniqueID": "SMAPI.SaveBackup",
|
||||
"EntryDll": "SaveBackup.dll",
|
||||
"MinimumApiVersion": "3.6.2"
|
||||
"MinimumApiVersion": "3.7.0"
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ namespace SMAPI.Tests.Core
|
||||
new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: key => null);
|
||||
|
||||
// assert
|
||||
mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny<string>()), Times.Once, "The validation did not fail the metadata.");
|
||||
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.");
|
||||
}
|
||||
|
||||
[Test(Description = "Assert that validation fails when the minimum API version is higher than the current SMAPI version.")]
|
||||
@@ -169,7 +169,7 @@ namespace SMAPI.Tests.Core
|
||||
new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: key => null);
|
||||
|
||||
// assert
|
||||
mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny<string>()), Times.Once, "The validation did not fail the metadata.");
|
||||
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.");
|
||||
}
|
||||
|
||||
[Test(Description = "Assert that validation fails when the manifest references a DLL that does not exist.")]
|
||||
@@ -183,7 +183,7 @@ namespace SMAPI.Tests.Core
|
||||
new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: key => null);
|
||||
|
||||
// assert
|
||||
mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny<string>()), Times.Once, "The validation did not fail the metadata.");
|
||||
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.");
|
||||
}
|
||||
|
||||
[Test(Description = "Assert that validation fails when multiple mods have the same unique ID.")]
|
||||
@@ -200,8 +200,8 @@ namespace SMAPI.Tests.Core
|
||||
new ModResolver().ValidateManifests(new[] { modA.Object, modB.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: key => null);
|
||||
|
||||
// assert
|
||||
modA.Verify(p => p.SetStatus(ModMetadataStatus.Failed, 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<string>()), Times.Once, "The validation did not fail the second mod with a unique ID.");
|
||||
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.");
|
||||
}
|
||||
|
||||
[Test(Description = "Assert that validation fails when the manifest references a DLL that does not exist.")]
|
||||
@@ -367,9 +367,9 @@ namespace SMAPI.Tests.Core
|
||||
Assert.AreEqual(5, mods.Length, 0, "Expected to get the same number of mods input.");
|
||||
Assert.AreSame(modA.Object, mods[0], "The load order is incorrect: mod A should be first since it's needed by mod B.");
|
||||
Assert.AreSame(modB.Object, mods[1], "The load order is incorrect: mod B should be second since it needs mod A.");
|
||||
modC.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny<string>()), Times.Once, "Mod C was expected to fail since it's part of a dependency loop.");
|
||||
modD.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny<string>()), Times.Once, "Mod D was expected to fail since it's part of a dependency loop.");
|
||||
modE.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny<string>()), Times.Once, "Mod E was expected to fail since it's part of a dependency loop.");
|
||||
modC.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny<ModFailReason>(), It.IsAny<string>(), It.IsAny<string>()), Times.Once, "Mod C was expected to fail since it's part of a dependency loop.");
|
||||
modD.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny<ModFailReason>(), It.IsAny<string>(), It.IsAny<string>()), Times.Once, "Mod D was expected to fail since it's part of a dependency loop.");
|
||||
modE.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny<ModFailReason>(), It.IsAny<string>(), It.IsAny<string>()), Times.Once, "Mod E was expected to fail since it's part of a dependency loop.");
|
||||
}
|
||||
|
||||
[Test(Description = "Assert that dependencies are sorted correctly even if some of the mods failed during metadata loading.")]
|
||||
@@ -408,7 +408,7 @@ namespace SMAPI.Tests.Core
|
||||
|
||||
// assert
|
||||
Assert.AreEqual(2, mods.Length, 0, "Expected to get the same number of mods input.");
|
||||
modB.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny<string>()), Times.Once, "Mod B unexpectedly didn't fail even though it needs a newer version of Mod A.");
|
||||
modB.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny<ModFailReason>(), It.IsAny<string>(), It.IsAny<string>()), Times.Once, "Mod B unexpectedly didn't fail even though it needs a newer version of Mod A.");
|
||||
}
|
||||
|
||||
[Test(Description = "Assert that dependencies are accepted if they meet the minimum version.")]
|
||||
@@ -525,8 +525,8 @@ namespace SMAPI.Tests.Core
|
||||
if (allowStatusChange)
|
||||
{
|
||||
mod
|
||||
.Setup(p => p.SetStatus(It.IsAny<ModMetadataStatus>(), It.IsAny<string>()))
|
||||
.Callback<ModMetadataStatus, string>((status, message) => Console.WriteLine($"<{manifest.UniqueID} changed status: [{status}] {message}"))
|
||||
.Setup(p => p.SetStatus(It.IsAny<ModMetadataStatus>(), It.IsAny<ModFailReason>(), It.IsAny<string>(), It.IsAny<string>()))
|
||||
.Callback<ModMetadataStatus, ModFailReason, string, string>((status, failReason, message, errorDetails) => Console.WriteLine($"<{manifest.UniqueID} changed status: [{status}] {message}\n{failReason}\n{errorDetails}"))
|
||||
.Returns(mod.Object);
|
||||
}
|
||||
return mod;
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
using NUnit.Framework;
|
||||
using StardewModdingAPI.Toolkit.Utilities;
|
||||
|
||||
namespace SMAPI.Tests.Utilities
|
||||
{
|
||||
/// <summary>Unit tests for <see cref="PathUtilities"/>.</summary>
|
||||
[TestFixture]
|
||||
internal class PathUtilitiesTests
|
||||
{
|
||||
/*********
|
||||
** Sample data
|
||||
*********/
|
||||
/// <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",
|
||||
|
||||
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"
|
||||
},
|
||||
|
||||
// Windows absolute path (with trailing slash)
|
||||
new SamplePath
|
||||
{
|
||||
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\" },
|
||||
|
||||
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",
|
||||
|
||||
Segments = new [] { "Content", "Characters", "Dialogue", "Abigail" },
|
||||
SegmentsLimit3 = new [] { "Content", "Characters", @"Dialogue\Abigail" },
|
||||
|
||||
NormalizedOnWindows = @"Content\Characters\Dialogue\Abigail",
|
||||
NormalizedOnUnix = @"Content/Characters/Dialogue/Abigail"
|
||||
},
|
||||
|
||||
// Windows relative path (with directory climbing)
|
||||
new SamplePath
|
||||
{
|
||||
OriginalPath = @"..\..\Content",
|
||||
|
||||
Segments = new [] { "..", "..", "Content" },
|
||||
SegmentsLimit3 = new [] { "..", "..", "Content" },
|
||||
|
||||
NormalizedOnWindows = @"..\..\Content",
|
||||
NormalizedOnUnix = @"../../Content"
|
||||
},
|
||||
|
||||
// Windows UNC path
|
||||
new SamplePath
|
||||
{
|
||||
OriginalPath = @"\\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
|
||||
},
|
||||
|
||||
// Linux absolute path
|
||||
new SamplePath
|
||||
{
|
||||
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" },
|
||||
|
||||
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/",
|
||||
|
||||
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/"
|
||||
},
|
||||
|
||||
// Linux absolute path (with ~)
|
||||
new SamplePath
|
||||
{
|
||||
OriginalPath = @"~/.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"
|
||||
},
|
||||
|
||||
// Linux relative path
|
||||
new SamplePath
|
||||
{
|
||||
OriginalPath = @"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"
|
||||
},
|
||||
|
||||
// Linux relative path (with directory climbing)
|
||||
new SamplePath
|
||||
{
|
||||
OriginalPath = @"../../Content",
|
||||
|
||||
Segments = new [] { "..", "..", "Content" },
|
||||
SegmentsLimit3 = new [] { "..", "..", "Content" },
|
||||
|
||||
NormalizedOnWindows = @"..\..\Content",
|
||||
NormalizedOnUnix = @"../../Content"
|
||||
},
|
||||
|
||||
// Mixed directory separators
|
||||
new SamplePath
|
||||
{
|
||||
OriginalPath = @"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"
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
/*********
|
||||
** Unit tests
|
||||
*********/
|
||||
/****
|
||||
** GetSegments
|
||||
****/
|
||||
[Test(Description = "Assert that PathUtilities.GetSegments splits paths correctly.")]
|
||||
[TestCaseSource(nameof(PathUtilitiesTests.SamplePaths))]
|
||||
public void GetSegments(SamplePath path)
|
||||
{
|
||||
// act
|
||||
string[] segments = PathUtilities.GetSegments(path.OriginalPath);
|
||||
|
||||
// assert
|
||||
Assert.AreEqual(path.Segments, segments);
|
||||
}
|
||||
|
||||
[Test(Description = "Assert that PathUtilities.GetSegments splits paths correctly when given a limit.")]
|
||||
[TestCaseSource(nameof(PathUtilitiesTests.SamplePaths))]
|
||||
public void GetSegments_WithLimit(SamplePath path)
|
||||
{
|
||||
// act
|
||||
string[] segments = PathUtilities.GetSegments(path.OriginalPath, 3);
|
||||
|
||||
// assert
|
||||
Assert.AreEqual(path.SegmentsLimit3, segments);
|
||||
}
|
||||
|
||||
/****
|
||||
** NormalizePathSeparators
|
||||
****/
|
||||
[Test(Description = "Assert that PathUtilities.NormalizePathSeparators normalizes paths correctly.")]
|
||||
[TestCaseSource(nameof(PathUtilitiesTests.SamplePaths))]
|
||||
public void NormalizePathSeparators(SamplePath path)
|
||||
{
|
||||
// act
|
||||
string normalized = PathUtilities.NormalizePathSeparators(path.OriginalPath);
|
||||
|
||||
// assert
|
||||
#if SMAPI_FOR_WINDOWS
|
||||
Assert.AreEqual(path.NormalizedOnWindows, normalized);
|
||||
#else
|
||||
Assert.AreEqual(path.NormalizedOnUnix, normalized);
|
||||
#endif
|
||||
}
|
||||
|
||||
/****
|
||||
** GetRelativePath
|
||||
****/
|
||||
[Test(Description = "Assert that PathUtilities.GetRelativePath returns the expected values.")]
|
||||
#if SMAPI_FOR_WINDOWS
|
||||
[TestCase(
|
||||
@"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley",
|
||||
@"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley\Mods\Automate",
|
||||
ExpectedResult = @"Mods\Automate"
|
||||
)]
|
||||
[TestCase(
|
||||
@"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley\Mods\Automate",
|
||||
@"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley\Content",
|
||||
ExpectedResult = @"..\..\Content"
|
||||
)]
|
||||
[TestCase(
|
||||
@"C:\Program Files (x86)\Steam\steamapps\common\Stardew Valley\Mods\Automate",
|
||||
@"D:\another-drive",
|
||||
ExpectedResult = @"D:\another-drive"
|
||||
)]
|
||||
[TestCase(
|
||||
@"\\parent\unc",
|
||||
@"\\parent\unc\path\to\child",
|
||||
ExpectedResult = @"path\to\child"
|
||||
)]
|
||||
[TestCase(
|
||||
@"\\parent\unc",
|
||||
@"\\adjacent\unc",
|
||||
ExpectedResult = @"\\adjacent\unc"
|
||||
)]
|
||||
[TestCase(
|
||||
@"C:\same\path",
|
||||
@"C:\same\path",
|
||||
ExpectedResult = @"."
|
||||
)]
|
||||
[TestCase(
|
||||
@"C:\parent",
|
||||
@"C:\PARENT\child",
|
||||
ExpectedResult = @"child"
|
||||
)]
|
||||
#else
|
||||
[TestCase(
|
||||
@"~/.steam/steam/steamapps/common/Stardew Valley",
|
||||
@"~/.steam/steam/steamapps/common/Stardew Valley/Mods/Automate",
|
||||
ExpectedResult = @"Mods/Automate"
|
||||
)]
|
||||
[TestCase(
|
||||
@"~/.steam/steam/steamapps/common/Stardew Valley/Mods/Automate",
|
||||
@"~/.steam/steam/steamapps/common/Stardew Valley/Content",
|
||||
ExpectedResult = @"../../Content"
|
||||
)]
|
||||
[TestCase(
|
||||
@"~/.steam/steam/steamapps/common/Stardew Valley/Mods/Automate",
|
||||
@"/mnt/another-drive",
|
||||
ExpectedResult = @"/mnt/another-drive"
|
||||
)]
|
||||
[TestCase(
|
||||
@"~/same/path",
|
||||
@"~/same/path",
|
||||
ExpectedResult = @"."
|
||||
)]
|
||||
[TestCase(
|
||||
@"~/parent",
|
||||
@"~/PARENT/child",
|
||||
ExpectedResult = @"child" // note: incorrect on Linux and sometimes MacOS, but not worth the complexity of detecting whether the filesystem is case-sensitive for SMAPI's purposes
|
||||
)]
|
||||
#endif
|
||||
public string GetRelativePath(string sourceDir, string targetPath)
|
||||
{
|
||||
return PathUtilities.GetRelativePath(sourceDir, targetPath);
|
||||
}
|
||||
|
||||
|
||||
/*********
|
||||
** Private classes
|
||||
*********/
|
||||
public class SamplePath
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
#if SMAPI_FOR_WINDOWS
|
||||
using System.Management;
|
||||
#endif
|
||||
using System.Runtime.InteropServices;
|
||||
using StardewModdingAPI.Toolkit.Utilities;
|
||||
|
||||
namespace StardewModdingAPI.Toolkit.Framework
|
||||
{
|
||||
/// <summary>Provides low-level methods for fetching environment information.</summary>
|
||||
/// <remarks>This is used by the SMAPI core before the toolkit DLL is available; most code should use <see cref="EnvironmentUtility"/> instead.</remarks>
|
||||
internal static class LowLevelEnvironmentUtility
|
||||
{
|
||||
/*********
|
||||
** Fields
|
||||
*********/
|
||||
/// <summary>Get the OS name from the system uname command.</summary>
|
||||
/// <param name="buffer">The buffer to fill with the resulting string.</param>
|
||||
[DllImport("libc")]
|
||||
static extern int uname(IntPtr buffer);
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Detect the current OS.</summary>
|
||||
public static string DetectPlatform()
|
||||
{
|
||||
switch (Environment.OSVersion.Platform)
|
||||
{
|
||||
case PlatformID.MacOSX:
|
||||
return nameof(Platform.Mac);
|
||||
|
||||
case PlatformID.Unix when LowLevelEnvironmentUtility.IsRunningAndroid():
|
||||
return nameof(Platform.Android);
|
||||
|
||||
case PlatformID.Unix when LowLevelEnvironmentUtility.IsRunningMac():
|
||||
return nameof(Platform.Mac);
|
||||
|
||||
case PlatformID.Unix:
|
||||
return nameof(Platform.Linux);
|
||||
|
||||
default:
|
||||
return nameof(Platform.Windows);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>Get the human-readable OS name and version.</summary>
|
||||
/// <param name="platform">The current platform.</param>
|
||||
[SuppressMessage("ReSharper", "EmptyGeneralCatchClause", Justification = "Error suppressed deliberately to fallback to default behaviour.")]
|
||||
public static string GetFriendlyPlatformName(string platform)
|
||||
{
|
||||
#if SMAPI_FOR_WINDOWS
|
||||
try
|
||||
{
|
||||
return new ManagementObjectSearcher("SELECT Caption FROM Win32_OperatingSystem")
|
||||
.Get()
|
||||
.Cast<ManagementObject>()
|
||||
.Select(entry => entry.GetPropertyValue("Caption").ToString())
|
||||
.FirstOrDefault();
|
||||
}
|
||||
catch { }
|
||||
#endif
|
||||
|
||||
string name = Environment.OSVersion.ToString();
|
||||
switch (platform)
|
||||
{
|
||||
case nameof(Platform.Android):
|
||||
name = $"Android {name}";
|
||||
break;
|
||||
|
||||
case nameof(Platform.Mac):
|
||||
name = $"MacOS {name}";
|
||||
break;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
/// <summary>Get the name of the Stardew Valley executable.</summary>
|
||||
/// <param name="platform">The current platform.</param>
|
||||
public static string GetExecutableName(string platform)
|
||||
{
|
||||
return platform == nameof(Platform.Windows)
|
||||
? "Stardew Valley.exe"
|
||||
: "StardewValley.exe";
|
||||
}
|
||||
|
||||
/// <summary>Get whether the platform uses Mono.</summary>
|
||||
/// <param name="platform">The current platform.</param>
|
||||
public static bool IsMono(string platform)
|
||||
{
|
||||
return platform == nameof(Platform.Linux) || platform == nameof(Platform.Mac);
|
||||
}
|
||||
|
||||
|
||||
/*********
|
||||
** Private methods
|
||||
*********/
|
||||
/// <summary>Detect whether the code is running on Android.</summary>
|
||||
/// <remarks>
|
||||
/// This code is derived from https://stackoverflow.com/a/47521647/262123. It detects Android by calling the
|
||||
/// <c>getprop</c> system command to check for an Android-specific property.
|
||||
/// </remarks>
|
||||
private static bool IsRunningAndroid()
|
||||
{
|
||||
using Process process = new Process
|
||||
{
|
||||
StartInfo =
|
||||
{
|
||||
FileName = "getprop",
|
||||
Arguments = "ro.build.user",
|
||||
RedirectStandardOutput = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
process.Start();
|
||||
string output = process.StandardOutput.ReadToEnd();
|
||||
return !string.IsNullOrWhiteSpace(output);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Detect whether the code is running on Mac.</summary>
|
||||
/// <remarks>
|
||||
/// This code is derived from the Mono project (see System.Windows.Forms/System.Windows.Forms/XplatUI.cs). It detects Mac by calling the
|
||||
/// <c>uname</c> system command and checking the response, which is always 'Darwin' for MacOS.
|
||||
/// </remarks>
|
||||
private static bool IsRunningMac()
|
||||
{
|
||||
IntPtr buffer = IntPtr.Zero;
|
||||
try
|
||||
{
|
||||
buffer = Marshal.AllocHGlobal(8192);
|
||||
if (LowLevelEnvironmentUtility.uname(buffer) == 0)
|
||||
{
|
||||
string os = Marshal.PtrToStringAnsi(buffer);
|
||||
return os == "Darwin";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false; // default to Linux
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (buffer != IntPtr.Zero)
|
||||
Marshal.FreeHGlobal(buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,22 +25,22 @@ namespace StardewModdingAPI.Toolkit
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The major version incremented for major API changes.</summary>
|
||||
/// <inheritdoc />
|
||||
public int MajorVersion { get; }
|
||||
|
||||
/// <summary>The minor version incremented for backwards-compatible changes.</summary>
|
||||
/// <inheritdoc />
|
||||
public int MinorVersion { get; }
|
||||
|
||||
/// <summary>The patch version for backwards-compatible bug fixes.</summary>
|
||||
/// <inheritdoc />
|
||||
public int PatchVersion { get; }
|
||||
|
||||
/// <summary>The platform release. This is a non-standard semver extension used by Stardew Valley on ported platforms to represent platform-specific patches to a ported version, represented as a fourth number in the version string.</summary>
|
||||
public int PlatformRelease { get; }
|
||||
|
||||
/// <summary>An optional prerelease tag.</summary>
|
||||
/// <inheritdoc />
|
||||
public string PrereleaseTag { get; }
|
||||
|
||||
/// <summary>Optional build metadata. This is ignored when determining version precedence.</summary>
|
||||
/// <inheritdoc />
|
||||
public string BuildMetadata { get; }
|
||||
|
||||
|
||||
@@ -103,9 +103,7 @@ namespace StardewModdingAPI.Toolkit
|
||||
this.AssertValid();
|
||||
}
|
||||
|
||||
/// <summary>Get an integer indicating whether this version precedes (less than 0), supersedes (more than 0), or is equivalent to (0) the specified version.</summary>
|
||||
/// <param name="other">The version to compare with this instance.</param>
|
||||
/// <exception cref="ArgumentNullException">The <paramref name="other"/> value is null.</exception>
|
||||
/// <inheritdoc />
|
||||
public int CompareTo(ISemanticVersion other)
|
||||
{
|
||||
if (other == null)
|
||||
@@ -113,68 +111,55 @@ namespace StardewModdingAPI.Toolkit
|
||||
return this.CompareTo(other.MajorVersion, other.MinorVersion, other.PatchVersion, (other as SemanticVersion)?.PlatformRelease ?? 0, other.PrereleaseTag);
|
||||
}
|
||||
|
||||
/// <summary>Indicates whether the current object is equal to another object of the same type.</summary>
|
||||
/// <returns>true if the current object is equal to the <paramref name="other" /> parameter; otherwise, false.</returns>
|
||||
/// <param name="other">An object to compare with this object.</param>
|
||||
/// <inheritdoc />
|
||||
public bool Equals(ISemanticVersion other)
|
||||
{
|
||||
return other != null && this.CompareTo(other) == 0;
|
||||
}
|
||||
|
||||
/// <summary>Whether this is a prerelease version.</summary>
|
||||
/// <inheritdoc />
|
||||
public bool IsPrerelease()
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(this.PrereleaseTag);
|
||||
}
|
||||
|
||||
/// <summary>Get whether this version is older than the specified version.</summary>
|
||||
/// <param name="other">The version to compare with this instance.</param>
|
||||
/// <inheritdoc />
|
||||
public bool IsOlderThan(ISemanticVersion other)
|
||||
{
|
||||
return this.CompareTo(other) < 0;
|
||||
}
|
||||
|
||||
/// <summary>Get whether this version is older than the specified version.</summary>
|
||||
/// <param name="other">The version to compare with this instance.</param>
|
||||
/// <exception cref="FormatException">The specified version is not a valid semantic version.</exception>
|
||||
/// <inheritdoc />
|
||||
public bool IsOlderThan(string other)
|
||||
{
|
||||
return this.IsOlderThan(new SemanticVersion(other, allowNonStandard: true));
|
||||
}
|
||||
|
||||
/// <summary>Get whether this version is newer than the specified version.</summary>
|
||||
/// <param name="other">The version to compare with this instance.</param>
|
||||
/// <inheritdoc />
|
||||
public bool IsNewerThan(ISemanticVersion other)
|
||||
{
|
||||
return this.CompareTo(other) > 0;
|
||||
}
|
||||
|
||||
/// <summary>Get whether this version is newer than the specified version.</summary>
|
||||
/// <param name="other">The version to compare with this instance.</param>
|
||||
/// <exception cref="FormatException">The specified version is not a valid semantic version.</exception>
|
||||
/// <inheritdoc />
|
||||
public bool IsNewerThan(string other)
|
||||
{
|
||||
return this.IsNewerThan(new SemanticVersion(other, allowNonStandard: true));
|
||||
}
|
||||
|
||||
/// <summary>Get whether this version is between two specified versions (inclusively).</summary>
|
||||
/// <param name="min">The minimum version.</param>
|
||||
/// <param name="max">The maximum version.</param>
|
||||
/// <inheritdoc />
|
||||
public bool IsBetween(ISemanticVersion min, ISemanticVersion max)
|
||||
{
|
||||
return this.CompareTo(min) >= 0 && this.CompareTo(max) <= 0;
|
||||
}
|
||||
|
||||
/// <summary>Get whether this version is between two specified versions (inclusively).</summary>
|
||||
/// <param name="min">The minimum version.</param>
|
||||
/// <param name="max">The maximum version.</param>
|
||||
/// <exception cref="FormatException">One of the specified versions is not a valid semantic version.</exception>
|
||||
/// <inheritdoc />
|
||||
public bool IsBetween(string min, string max)
|
||||
{
|
||||
return this.IsBetween(new SemanticVersion(min, allowNonStandard: true), new SemanticVersion(max, allowNonStandard: true));
|
||||
}
|
||||
|
||||
/// <summary>Get a string representation of the version.</summary>
|
||||
/// <inheritdoc cref="ISemanticVersion.ToString" />
|
||||
public override string ToString()
|
||||
{
|
||||
string version = $"{this.MajorVersion}.{this.MinorVersion}.{this.PatchVersion}";
|
||||
@@ -187,7 +172,7 @@ namespace StardewModdingAPI.Toolkit
|
||||
return version;
|
||||
}
|
||||
|
||||
/// <summary>Whether the version uses non-standard extensions, like four-part game versions on some platforms.</summary>
|
||||
/// <inheritdoc />
|
||||
public bool IsNonStandard()
|
||||
{
|
||||
return this.PlatformRelease != 0;
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
#if SMAPI_FOR_WINDOWS
|
||||
using System.Management;
|
||||
#endif
|
||||
using System.Runtime.InteropServices;
|
||||
using StardewModdingAPI.Toolkit.Framework;
|
||||
|
||||
namespace StardewModdingAPI.Toolkit.Utilities
|
||||
{
|
||||
@@ -18,11 +13,6 @@ namespace StardewModdingAPI.Toolkit.Utilities
|
||||
/// <summary>The cached platform.</summary>
|
||||
private static Platform? CachedPlatform;
|
||||
|
||||
/// <summary>Get the OS name from the system uname command.</summary>
|
||||
/// <param name="buffer">The buffer to fill with the resulting string.</param>
|
||||
[DllImport("libc")]
|
||||
static extern int uname(IntPtr buffer);
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
@@ -30,7 +20,15 @@ namespace StardewModdingAPI.Toolkit.Utilities
|
||||
/// <summary>Detect the current OS.</summary>
|
||||
public static Platform DetectPlatform()
|
||||
{
|
||||
return EnvironmentUtility.CachedPlatform ??= EnvironmentUtility.DetectPlatformImpl();
|
||||
Platform? platform = EnvironmentUtility.CachedPlatform;
|
||||
|
||||
if (platform == null)
|
||||
{
|
||||
string rawPlatform = LowLevelEnvironmentUtility.DetectPlatform();
|
||||
EnvironmentUtility.CachedPlatform = platform = (Platform)Enum.Parse(typeof(Platform), rawPlatform, ignoreCase: true);
|
||||
}
|
||||
|
||||
return platform.Value;
|
||||
}
|
||||
|
||||
|
||||
@@ -39,132 +37,21 @@ namespace StardewModdingAPI.Toolkit.Utilities
|
||||
[SuppressMessage("ReSharper", "EmptyGeneralCatchClause", Justification = "Error suppressed deliberately to fallback to default behaviour.")]
|
||||
public static string GetFriendlyPlatformName(Platform platform)
|
||||
{
|
||||
#if SMAPI_FOR_WINDOWS
|
||||
try
|
||||
{
|
||||
return new ManagementObjectSearcher("SELECT Caption FROM Win32_OperatingSystem")
|
||||
.Get()
|
||||
.Cast<ManagementObject>()
|
||||
.Select(entry => entry.GetPropertyValue("Caption").ToString())
|
||||
.FirstOrDefault();
|
||||
}
|
||||
catch { }
|
||||
#endif
|
||||
|
||||
string name = Environment.OSVersion.ToString();
|
||||
switch (platform)
|
||||
{
|
||||
case Platform.Android:
|
||||
name = $"Android {name}";
|
||||
break;
|
||||
|
||||
case Platform.Mac:
|
||||
name = $"MacOS {name}";
|
||||
break;
|
||||
}
|
||||
return name;
|
||||
return LowLevelEnvironmentUtility.GetFriendlyPlatformName(platform.ToString());
|
||||
}
|
||||
|
||||
/// <summary>Get the name of the Stardew Valley executable.</summary>
|
||||
/// <param name="platform">The current platform.</param>
|
||||
public static string GetExecutableName(Platform platform)
|
||||
{
|
||||
return platform == Platform.Windows
|
||||
? "Stardew Valley.exe"
|
||||
: "StardewValley.exe";
|
||||
return LowLevelEnvironmentUtility.GetExecutableName(platform.ToString());
|
||||
}
|
||||
|
||||
/// <summary>Get whether the platform uses Mono.</summary>
|
||||
/// <param name="platform">The current platform.</param>
|
||||
public static bool IsMono(this Platform platform)
|
||||
{
|
||||
return platform == Platform.Linux || platform == Platform.Mac;
|
||||
}
|
||||
|
||||
|
||||
/*********
|
||||
** Private methods
|
||||
*********/
|
||||
/// <summary>Detect the current OS.</summary>
|
||||
private static Platform DetectPlatformImpl()
|
||||
{
|
||||
switch (Environment.OSVersion.Platform)
|
||||
{
|
||||
case PlatformID.MacOSX:
|
||||
return Platform.Mac;
|
||||
|
||||
case PlatformID.Unix when EnvironmentUtility.IsRunningAndroid():
|
||||
return Platform.Android;
|
||||
|
||||
case PlatformID.Unix when EnvironmentUtility.IsRunningMac():
|
||||
return Platform.Mac;
|
||||
|
||||
case PlatformID.Unix:
|
||||
return Platform.Linux;
|
||||
|
||||
default:
|
||||
return Platform.Windows;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Detect whether the code is running on Android.</summary>
|
||||
/// <remarks>
|
||||
/// This code is derived from https://stackoverflow.com/a/47521647/262123. It detects Android by calling the
|
||||
/// <c>getprop</c> system command to check for an Android-specific property.
|
||||
/// </remarks>
|
||||
private static bool IsRunningAndroid()
|
||||
{
|
||||
using Process process = new Process
|
||||
{
|
||||
StartInfo =
|
||||
{
|
||||
FileName = "getprop",
|
||||
Arguments = "ro.build.user",
|
||||
RedirectStandardOutput = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
process.Start();
|
||||
string output = process.StandardOutput.ReadToEnd();
|
||||
return !string.IsNullOrWhiteSpace(output);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Detect whether the code is running on Mac.</summary>
|
||||
/// <remarks>
|
||||
/// This code is derived from the Mono project (see System.Windows.Forms/System.Windows.Forms/XplatUI.cs). It detects Mac by calling the
|
||||
/// <c>uname</c> system command and checking the response, which is always 'Darwin' for MacOS.
|
||||
/// </remarks>
|
||||
private static bool IsRunningMac()
|
||||
{
|
||||
IntPtr buffer = IntPtr.Zero;
|
||||
try
|
||||
{
|
||||
buffer = Marshal.AllocHGlobal(8192);
|
||||
if (EnvironmentUtility.uname(buffer) == 0)
|
||||
{
|
||||
string os = Marshal.PtrToStringAnsi(buffer);
|
||||
return os == "Darwin";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false; // default to Linux
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (buffer != IntPtr.Zero)
|
||||
Marshal.FreeHGlobal(buffer);
|
||||
}
|
||||
return LowLevelEnvironmentUtility.IsMono(platform.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,13 @@ namespace StardewModdingAPI.Toolkit.Utilities
|
||||
/// <summary>Provides utilities for normalizing file paths.</summary>
|
||||
public static class PathUtilities
|
||||
{
|
||||
/*********
|
||||
** Fields
|
||||
*********/
|
||||
/// <summary>The root prefix for a Windows UNC path.</summary>
|
||||
private const string WindowsUncRoot = @"\\";
|
||||
|
||||
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
@@ -25,6 +32,7 @@ namespace StardewModdingAPI.Toolkit.Utilities
|
||||
/// <summary>Get the segments from a path (e.g. <c>/usr/bin/example</c> => <c>usr</c>, <c>bin</c>, and <c>example</c>).</summary>
|
||||
/// <param name="path">The path to split.</param>
|
||||
/// <param name="limit">The number of segments to match. Any additional segments will be merged into the last returned part.</param>
|
||||
[Pure]
|
||||
public static string[] GetSegments(string path, int? limit = null)
|
||||
{
|
||||
return limit.HasValue
|
||||
@@ -37,16 +45,20 @@ namespace StardewModdingAPI.Toolkit.Utilities
|
||||
[Pure]
|
||||
public static string NormalizePathSeparators(string path)
|
||||
{
|
||||
string[] parts = PathUtilities.GetSegments(path);
|
||||
string normalized = string.Join(PathUtilities.PreferredPathSeparator, parts);
|
||||
if (path.StartsWith(PathUtilities.PreferredPathSeparator))
|
||||
normalized = PathUtilities.PreferredPathSeparator + normalized; // keep root slash
|
||||
return normalized;
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
return path;
|
||||
|
||||
return string.Join(PathUtilities.PreferredPathSeparator, path.Split(PathUtilities.PossiblePathSeparators));
|
||||
}
|
||||
|
||||
/// <summary>Get a directory or file path relative to a given source path.</summary>
|
||||
/// <summary>Get a directory or file path relative to a given source path. If no relative path is possible (e.g. the paths are on different drives), an absolute path is returned.</summary>
|
||||
/// <param name="sourceDir">The source folder path.</param>
|
||||
/// <param name="targetPath">The target folder or file path.</param>
|
||||
/// <remarks>
|
||||
///
|
||||
/// NOTE: this is a heuristic implementation that works in the cases SMAPI needs it for, but it doesn't handle all edge cases (e.g. case-sensitivity on Linux, or traversing between UNC paths on Windows). This should be replaced with the more comprehensive <c>Path.GetRelativePath</c> if the game ever migrates to .NET Core.
|
||||
///
|
||||
/// </remarks>
|
||||
[Pure]
|
||||
public static string GetRelativePath(string sourceDir, string targetPath)
|
||||
{
|
||||
@@ -58,13 +70,27 @@ namespace StardewModdingAPI.Toolkit.Utilities
|
||||
|
||||
// get relative path
|
||||
string relative = PathUtilities.NormalizePathSeparators(Uri.UnescapeDataString(from.MakeRelativeUri(to).ToString()));
|
||||
|
||||
// normalize
|
||||
if (relative == "")
|
||||
relative = "./";
|
||||
relative = ".";
|
||||
else
|
||||
{
|
||||
// trim trailing slash from URL
|
||||
if (relative.EndsWith(PathUtilities.PreferredPathSeparator))
|
||||
relative = relative.Substring(0, relative.Length - PathUtilities.PreferredPathSeparator.Length);
|
||||
|
||||
// fix root
|
||||
if (relative.StartsWith("file:") && !targetPath.Contains("file:"))
|
||||
relative = relative.Substring("file:".Length);
|
||||
}
|
||||
|
||||
return relative;
|
||||
}
|
||||
|
||||
/// <summary>Get whether a path is relative and doesn't try to climb out of its containing folder (e.g. doesn't contain <c>../</c>).</summary>
|
||||
/// <param name="path">The path to check.</param>
|
||||
[Pure]
|
||||
public static bool IsSafeRelativePath(string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
@@ -77,6 +103,7 @@ namespace StardewModdingAPI.Toolkit.Utilities
|
||||
|
||||
/// <summary>Get whether a string is a valid 'slug', containing only basic characters that are safe in all contexts (e.g. filenames, URLs, etc).</summary>
|
||||
/// <param name="str">The string to check.</param>
|
||||
[Pure]
|
||||
public static bool IsSlug(string str)
|
||||
{
|
||||
return !Regex.IsMatch(str, "[^a-z0-9_.-]", RegexOptions.IgnoreCase);
|
||||
|
||||
@@ -156,7 +156,7 @@ namespace StardewModdingAPI.Web.Controllers
|
||||
|
||||
// get unofficial version
|
||||
if (wikiEntry?.Compatibility.UnofficialVersion != null && this.IsNewer(wikiEntry.Compatibility.UnofficialVersion, main?.Version) && this.IsNewer(wikiEntry.Compatibility.UnofficialVersion, optional?.Version))
|
||||
unofficial = new ModEntryVersionModel(wikiEntry.Compatibility.UnofficialVersion, $"{this.Url.PlainAction("Index", "Mods")}#{wikiEntry.Anchor}");
|
||||
unofficial = new ModEntryVersionModel(wikiEntry.Compatibility.UnofficialVersion, $"{this.Url.PlainAction("Index", "Mods", absoluteUrl: true)}#{wikiEntry.Anchor}");
|
||||
|
||||
// get unofficial version for beta
|
||||
if (wikiEntry?.HasBetaInfo == true)
|
||||
@@ -166,7 +166,7 @@ namespace StardewModdingAPI.Web.Controllers
|
||||
if (wikiEntry.BetaCompatibility.UnofficialVersion != null)
|
||||
{
|
||||
unofficialForBeta = (wikiEntry.BetaCompatibility.UnofficialVersion != null && this.IsNewer(wikiEntry.BetaCompatibility.UnofficialVersion, main?.Version) && this.IsNewer(wikiEntry.BetaCompatibility.UnofficialVersion, optional?.Version))
|
||||
? new ModEntryVersionModel(wikiEntry.BetaCompatibility.UnofficialVersion, $"{this.Url.PlainAction("Index", "Mods")}#{wikiEntry.Anchor}")
|
||||
? new ModEntryVersionModel(wikiEntry.BetaCompatibility.UnofficialVersion, $"{this.Url.PlainAction("Index", "Mods", absoluteUrl: true)}#{wikiEntry.Anchor}")
|
||||
: null;
|
||||
}
|
||||
else
|
||||
@@ -198,9 +198,9 @@ namespace StardewModdingAPI.Web.Controllers
|
||||
List<ModEntryVersionModel> updates = new List<ModEntryVersionModel>();
|
||||
if (this.IsRecommendedUpdate(installedVersion, main?.Version, useBetaChannel: true))
|
||||
updates.Add(main);
|
||||
if (this.IsRecommendedUpdate(installedVersion, optional?.Version, useBetaChannel: installedVersion.IsPrerelease()))
|
||||
if (this.IsRecommendedUpdate(installedVersion, optional?.Version, useBetaChannel: installedVersion.IsPrerelease() || search.IsBroken))
|
||||
updates.Add(optional);
|
||||
if (this.IsRecommendedUpdate(installedVersion, unofficial?.Version, useBetaChannel: search.IsBroken))
|
||||
if (this.IsRecommendedUpdate(installedVersion, unofficial?.Version, useBetaChannel: true))
|
||||
updates.Add(unofficial);
|
||||
if (this.IsRecommendedUpdate(installedVersion, unofficialForBeta?.Version, useBetaChannel: apiVersion.IsPrerelease()))
|
||||
updates.Add(unofficialForBeta);
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace StardewModdingAPI.Web.Framework
|
||||
/****
|
||||
** View helpers
|
||||
****/
|
||||
/// <summary>Get a URL with the absolute path for an action method. Unlike <see cref="IUrlHelper.Action"/>, only the specified <paramref name="values"/> are added to the URL without merging values from the current HTTP request.</summary>
|
||||
/// <summary>Get a URL for an action method. Unlike <see cref="IUrlHelper.Action"/>, only the specified <paramref name="values"/> are added to the URL without merging values from the current HTTP request.</summary>
|
||||
/// <param name="helper">The URL helper to extend.</param>
|
||||
/// <param name="action">The name of the action method.</param>
|
||||
/// <param name="controller">The name of the controller.</param>
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
"title": "Format version",
|
||||
"description": "The format version. You should always use the latest version to enable the latest features and avoid obsolete behavior.",
|
||||
"type": "string",
|
||||
"const": "1.15.0",
|
||||
"const": "1.17.0",
|
||||
"@errorMessages": {
|
||||
"const": "Incorrect value '@value'. This should be set to the latest format version, currently '1.15.0'."
|
||||
"const": "Incorrect value '@value'. This should be set to the latest format version, currently '1.17.0'."
|
||||
}
|
||||
},
|
||||
"ConfigSchema": {
|
||||
@@ -102,7 +102,7 @@
|
||||
"title": "Action",
|
||||
"description": "The kind of change to make.",
|
||||
"type": "string",
|
||||
"enum": [ "Load", "EditImage", "EditData", "EditMap" ]
|
||||
"enum": [ "Load", "EditImage", "EditData", "EditMap", "Include" ]
|
||||
},
|
||||
"Target": {
|
||||
"title": "Target asset",
|
||||
@@ -124,6 +124,9 @@
|
||||
"title": "Enabled",
|
||||
"description": "Whether to apply this patch. Default true. This fields supports immutable tokens (e.g. config tokens) if they return true/false.",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [ "true", "false" ]
|
||||
@@ -131,15 +134,18 @@
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "\\{\\{[^{}]+\\}\\}"
|
||||
},
|
||||
{
|
||||
"type": "boolean"
|
||||
}
|
||||
],
|
||||
"@errorMessages": {
|
||||
"anyOf": "Invalid value; must be true, false, or a single token which evaluates to true or false."
|
||||
}
|
||||
},
|
||||
"Update": {
|
||||
"title": "Update",
|
||||
"description": "When the patch should update if it changed. The possible values are 'OnDayStart' and 'OnLocationChange' (defaults to OnDayStart).",
|
||||
"type": "string",
|
||||
"enum": [ "OnDayStart", "OnLocationChange" ]
|
||||
},
|
||||
"FromFile": {
|
||||
"title": "Source file",
|
||||
"description": "The relative file path in your content pack folder to load instead (like 'assets/dinosaur.png'). This can be a .json (data), .png (image), .tbin or .tmx (map), or .xnb file. This field supports tokens and capitalization doesn't matter.",
|
||||
@@ -327,9 +333,17 @@
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": [ "FromFile" ],
|
||||
"required": [ "FromFile", "Target" ],
|
||||
"propertyNames": {
|
||||
"enum": [ "Action", "Target", "LogName", "Enabled", "When", "FromFile" ]
|
||||
"enum": [
|
||||
"Action",
|
||||
"Enabled",
|
||||
"FromFile",
|
||||
"LogName",
|
||||
"Target",
|
||||
"Update",
|
||||
"When"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -340,9 +354,21 @@
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": [ "FromFile" ],
|
||||
"required": [ "FromFile", "Target" ],
|
||||
"propertyNames": {
|
||||
"enum": [ "Action", "Target", "LogName", "Enabled", "When", "FromFile", "FromArea", "ToArea", "PatchMode" ]
|
||||
"enum": [
|
||||
"Action",
|
||||
"Enabled",
|
||||
"FromFile",
|
||||
"LogName",
|
||||
"Target",
|
||||
"Update",
|
||||
"When",
|
||||
|
||||
"FromArea",
|
||||
"PatchMode",
|
||||
"ToArea"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -354,7 +380,19 @@
|
||||
},
|
||||
"then": {
|
||||
"propertyNames": {
|
||||
"enum": [ "Action", "Target", "LogName", "Enabled", "When", "FromFile", "Fields", "Entries", "MoveEntries" ]
|
||||
"enum": [
|
||||
"Action",
|
||||
"Enabled",
|
||||
"FromFile",
|
||||
"LogName",
|
||||
"Target",
|
||||
"Update",
|
||||
"When",
|
||||
|
||||
"Entries",
|
||||
"Fields",
|
||||
"MoveEntries"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -377,16 +415,44 @@
|
||||
}
|
||||
},
|
||||
"propertyNames": {
|
||||
"enum": [ "Action", "Target", "LogName", "Enabled", "When", "FromFile", "FromArea", "ToArea", "MapProperties", "MapTiles" ]
|
||||
"enum": [
|
||||
"Action",
|
||||
"Enabled",
|
||||
"FromFile",
|
||||
"LogName",
|
||||
"Target",
|
||||
"Update",
|
||||
"When",
|
||||
|
||||
"FromArea",
|
||||
"MapProperties",
|
||||
"MapTiles",
|
||||
"ToArea"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"Action": { "const": "Include" }
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": [ "FromFile" ],
|
||||
"propertyNames": {
|
||||
"enum": [
|
||||
"Action",
|
||||
"Enabled",
|
||||
"FromFile",
|
||||
"LogName",
|
||||
"Update",
|
||||
"When"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
"required": [ "Action", "Target" ],
|
||||
"@errorMessages": {
|
||||
"allOf": "$transparent"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"$schema": {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MemberCanBeMadeStatic_002ELocal/@EntryIndexedValue">DO_NOT_SHOW</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantNameQualifier/@EntryIndexedValue">HINT</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantTypeArgumentsOfMethod/@EntryIndexedValue">HINT</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=RemoveRedundantBraces/@EntryIndexedValue">DO_NOT_SHOW</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/StaticQualifier/STATIC_MEMBERS_QUALIFY_MEMBERS/@EntryValue">Field, Property, Event, Method</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/ThisQualifier/INSTANCE_MEMBERS_QUALIFY_MEMBERS/@EntryValue">Field, Property, Event, Method</s:String>
|
||||
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/LINE_FEED_AT_FILE_END/@EntryValue">True</s:Boolean>
|
||||
@@ -53,6 +54,7 @@
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=rewriter/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=rewriters/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=SMAPI/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=SMAPI_0027s/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=spawnable/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=spritesheet/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=stackable/@EntryIndexedValue">True</s:Boolean>
|
||||
|
||||
+38
-10
@@ -5,11 +5,42 @@ using System.Reflection;
|
||||
using StardewModdingAPI.Enums;
|
||||
using StardewModdingAPI.Framework;
|
||||
using StardewModdingAPI.Framework.ModLoading;
|
||||
using StardewModdingAPI.Toolkit.Framework;
|
||||
using StardewModdingAPI.Toolkit.Utilities;
|
||||
using StardewValley;
|
||||
|
||||
namespace StardewModdingAPI
|
||||
{
|
||||
/// <summary>Contains constants that are accessed before the game itself has been loaded.</summary>
|
||||
/// <remarks>Most code should use <see cref="Constants"/> instead of this class directly.</remarks>
|
||||
internal static class EarlyConstants
|
||||
{
|
||||
//
|
||||
// Note: this class *must not* depend on any external DLL beyond .NET Framework itself.
|
||||
// That includes the game or SMAPI toolkit, since it's accessed before those are loaded.
|
||||
//
|
||||
// Adding an external dependency may seem to work in some cases, but will prevent SMAPI
|
||||
// from showing a human-readable error if the game isn't available. To test this, just
|
||||
// rename "Stardew Valley.exe" in the game folder; you should see an error like "Oops!
|
||||
// SMAPI can't find the game", not a technical exception.
|
||||
//
|
||||
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The path to the game folder.</summary>
|
||||
public static string ExecutionPath { get; } = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
|
||||
|
||||
/// <summary>The absolute path to the folder containing SMAPI's internal files.</summary>
|
||||
public static readonly string InternalFilesPath = Path.Combine(EarlyConstants.ExecutionPath, "smapi-internal");
|
||||
|
||||
/// <summary>The target game platform.</summary>
|
||||
internal static GamePlatform Platform { get; } = (GamePlatform)Enum.Parse(typeof(GamePlatform), LowLevelEnvironmentUtility.DetectPlatform());
|
||||
|
||||
/// <summary>The game's assembly name.</summary>
|
||||
internal static string GameAssemblyName => EarlyConstants.Platform == GamePlatform.Windows ? "Stardew Valley" : "StardewValley";
|
||||
}
|
||||
|
||||
/// <summary>Contains SMAPI's constants and assumptions.</summary>
|
||||
public static class Constants
|
||||
{
|
||||
@@ -20,19 +51,19 @@ namespace StardewModdingAPI
|
||||
** Public
|
||||
****/
|
||||
/// <summary>SMAPI's current semantic version.</summary>
|
||||
public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.6.2");
|
||||
public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.7.0");
|
||||
|
||||
/// <summary>The minimum supported version of Stardew Valley.</summary>
|
||||
public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.4.1");
|
||||
|
||||
/// <summary>The maximum supported version of Stardew Valley.</summary>
|
||||
public static ISemanticVersion MaximumGameVersion { get; } = null;
|
||||
public static ISemanticVersion MaximumGameVersion { get; } = new GameVersion("1.4.5");
|
||||
|
||||
/// <summary>The target game platform.</summary>
|
||||
public static GamePlatform TargetPlatform => (GamePlatform)Constants.Platform;
|
||||
public static GamePlatform TargetPlatform { get; } = EarlyConstants.Platform;
|
||||
|
||||
/// <summary>The path to the game folder.</summary>
|
||||
public static string ExecutionPath { get; } = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
|
||||
public static string ExecutionPath { get; } = EarlyConstants.ExecutionPath;
|
||||
|
||||
/// <summary>The directory path containing Stardew Valley's app data.</summary>
|
||||
public static string DataPath { get; } = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "StardewValley");
|
||||
@@ -67,7 +98,7 @@ namespace StardewModdingAPI
|
||||
internal const string GamePerformanceCounterName = "<StardewValley>";
|
||||
|
||||
/// <summary>The absolute path to the folder containing SMAPI's internal files.</summary>
|
||||
internal static readonly string InternalFilesPath = Program.DllSearchPath;
|
||||
internal static readonly string InternalFilesPath = EarlyConstants.InternalFilesPath;
|
||||
|
||||
/// <summary>The file path for the SMAPI configuration file.</summary>
|
||||
internal static string ApiConfigPath => Path.Combine(Constants.InternalFilesPath, "config.json");
|
||||
@@ -105,11 +136,8 @@ namespace StardewModdingAPI
|
||||
/// <summary>The game's current semantic version.</summary>
|
||||
internal static ISemanticVersion GameVersion { get; } = new GameVersion(Game1.version);
|
||||
|
||||
/// <summary>The target game platform.</summary>
|
||||
internal static Platform Platform { get; } = EnvironmentUtility.DetectPlatform();
|
||||
|
||||
/// <summary>The game's assembly name.</summary>
|
||||
internal static string GameAssemblyName => Constants.Platform == Platform.Windows ? "Stardew Valley" : "StardewValley";
|
||||
/// <summary>The target game platform as a SMAPI toolkit constant.</summary>
|
||||
internal static Platform Platform { get; } = (Platform)Constants.TargetPlatform;
|
||||
|
||||
/// <summary>The language code for non-translated mod assets.</summary>
|
||||
internal static LocalizedContentManager.LanguageCode DefaultLanguage { get; } = LocalizedContentManager.LanguageCode.en;
|
||||
|
||||
@@ -12,4 +12,4 @@ namespace StardewModdingAPI.Events
|
||||
/// <summary>The stack size changed.</summary>
|
||||
StackChange
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace StardewModdingAPI.Events
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The number of ticks elapsed since the game started, including the current tick.</summary>
|
||||
public uint Ticks => SGame.TicksElapsed;
|
||||
public uint Ticks => SCore.TicksElapsed;
|
||||
|
||||
|
||||
/*********
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace StardewModdingAPI.Events
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The number of ticks elapsed since the game started, excluding the upcoming tick.</summary>
|
||||
public uint Ticks => SGame.TicksElapsed;
|
||||
public uint Ticks => SCore.TicksElapsed;
|
||||
|
||||
|
||||
/*********
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace StardewModdingAPI.Events
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The number of ticks elapsed since the game started, including the current tick.</summary>
|
||||
public uint Ticks => SGame.TicksElapsed;
|
||||
public uint Ticks => SCore.TicksElapsed;
|
||||
|
||||
/// <summary>Whether <see cref="Ticks"/> is a multiple of 60, which happens approximately once per second.</summary>
|
||||
public bool IsOneSecond => this.Ticks % 60 == 0;
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace StardewModdingAPI.Events
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The number of ticks elapsed since the game started, excluding the upcoming tick.</summary>
|
||||
public uint Ticks => SGame.TicksElapsed;
|
||||
public uint Ticks => SCore.TicksElapsed;
|
||||
|
||||
/// <summary>Whether <see cref="Ticks"/> is a multiple of 60, which happens approximately once per second.</summary>
|
||||
public bool IsOneSecond => this.Ticks % 60 == 0;
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace StardewModdingAPI.Events
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The number of ticks elapsed since the game started, including the current tick.</summary>
|
||||
public uint Ticks => SGame.TicksElapsed;
|
||||
public uint Ticks => SCore.TicksElapsed;
|
||||
|
||||
/// <summary>Whether <see cref="Ticks"/> is a multiple of 60, which happens approximately once per second.</summary>
|
||||
public bool IsOneSecond => this.Ticks % 60 == 0;
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace StardewModdingAPI.Events
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The number of ticks elapsed since the game started, excluding the upcoming tick.</summary>
|
||||
public uint Ticks => SGame.TicksElapsed;
|
||||
public uint Ticks => SCore.TicksElapsed;
|
||||
|
||||
/// <summary>Whether <see cref="Ticks"/> is a multiple of 60, which happens approximately once per second.</summary>
|
||||
public bool IsOneSecond => this.Ticks % 60 == 0;
|
||||
|
||||
@@ -22,7 +22,6 @@ namespace StardewModdingAPI.Events
|
||||
public bool IsLocalPlayer => this.Player.IsLocalPlayer;
|
||||
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
|
||||
@@ -119,6 +119,7 @@ namespace StardewModdingAPI.Framework
|
||||
command.Callback.Invoke(name, arguments);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace StardewModdingAPI.Framework.Content
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The content data being read.</summary>
|
||||
/// <inheritdoc />
|
||||
public TValue Data { get; protected set; }
|
||||
|
||||
|
||||
@@ -36,10 +36,7 @@ namespace StardewModdingAPI.Framework.Content
|
||||
this.OnDataReplaced = onDataReplaced;
|
||||
}
|
||||
|
||||
/// <summary>Replace the entire content value with the given value. This is generally not recommended, since it may break compatibility with other mods or different versions of the game.</summary>
|
||||
/// <param name="value">The new content value.</param>
|
||||
/// <exception cref="ArgumentNullException">The <paramref name="value"/> is null.</exception>
|
||||
/// <exception cref="InvalidCastException">The <paramref name="value"/>'s type is not compatible with the loaded asset's type.</exception>
|
||||
/// <inheritdoc />
|
||||
public void ReplaceWith(TValue value)
|
||||
{
|
||||
if (value == null)
|
||||
|
||||
@@ -28,13 +28,7 @@ namespace StardewModdingAPI.Framework.Content
|
||||
public AssetDataForImage(string locale, string assetName, Texture2D data, Func<string, string> getNormalizedPath, Action<Texture2D> onDataReplaced)
|
||||
: base(locale, assetName, data, getNormalizedPath, onDataReplaced) { }
|
||||
|
||||
/// <summary>Overwrite part of the image.</summary>
|
||||
/// <param name="source">The image to patch into the content.</param>
|
||||
/// <param name="sourceArea">The part of the <paramref name="source"/> to copy (or <c>null</c> to take the whole texture). This must be within the bounds of the <paramref name="source"/> texture.</param>
|
||||
/// <param name="targetArea">The part of the content to patch (or <c>null</c> to patch the whole texture). The original content within this area will be erased. This must be within the bounds of the existing spritesheet.</param>
|
||||
/// <param name="patchMode">Indicates how an image should be patched.</param>
|
||||
/// <exception cref="ArgumentNullException">One of the arguments is null.</exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException">The <paramref name="targetArea"/> is outside the bounds of the spritesheet.</exception>
|
||||
/// <inheritdoc />
|
||||
public void PatchImage(Texture2D source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMode patchMode = PatchMode.Replace)
|
||||
{
|
||||
// get texture
|
||||
@@ -104,10 +98,7 @@ namespace StardewModdingAPI.Framework.Content
|
||||
target.SetData(0, targetArea, sourceData, 0, pixelCount);
|
||||
}
|
||||
|
||||
/// <summary>Extend the image if needed to fit the given size. Note that this is an expensive operation, creates a new texture instance, and that extending a spritesheet horizontally may cause game errors or bugs.</summary>
|
||||
/// <param name="minWidth">The minimum texture width.</param>
|
||||
/// <param name="minHeight">The minimum texture height.</param>
|
||||
/// <returns>Whether the texture was resized.</returns>
|
||||
/// <inheritdoc />
|
||||
public bool ExtendImage(int minWidth, int minHeight)
|
||||
{
|
||||
if (this.Data.Width >= minWidth && this.Data.Height >= minHeight)
|
||||
|
||||
@@ -24,10 +24,7 @@ namespace StardewModdingAPI.Framework.Content
|
||||
public AssetDataForMap(string locale, string assetName, Map data, Func<string, string> getNormalizedPath, Action<Map> onDataReplaced)
|
||||
: base(locale, assetName, data, getNormalizedPath, onDataReplaced) { }
|
||||
|
||||
/// <summary>Copy layers, tiles, and tilesheets from another map onto the asset.</summary>
|
||||
/// <param name="source">The map from which to copy.</param>
|
||||
/// <param name="sourceArea">The tile area within the source map to copy, or <c>null</c> for the entire source map size. This must be within the bounds of the <paramref name="source"/> map.</param>
|
||||
/// <param name="targetArea">The tile area within the target map to overwrite, or <c>null</c> to patch the whole map. The original content within this area will be erased. This must be within the bounds of the existing map.</param>
|
||||
/// <inheritdoc />
|
||||
/// <remarks>Derived from <see cref="StardewValley.GameLocation.ApplyMapOverride"/> with a few changes:
|
||||
/// - can be applied directly to the maps when loading, before the location is created;
|
||||
/// - added support for source/target areas;
|
||||
|
||||
@@ -26,32 +26,25 @@ namespace StardewModdingAPI.Framework.Content
|
||||
public AssetDataForObject(IAssetInfo info, object data, Func<string, string> getNormalizedPath)
|
||||
: this(info.Locale, info.AssetName, data, getNormalizedPath) { }
|
||||
|
||||
/// <summary>Get a helper to manipulate the data as a dictionary.</summary>
|
||||
/// <typeparam name="TKey">The expected dictionary key.</typeparam>
|
||||
/// <typeparam name="TValue">The expected dictionary balue.</typeparam>
|
||||
/// <exception cref="InvalidOperationException">The content being read isn't a dictionary.</exception>
|
||||
/// <inheritdoc />
|
||||
public IAssetDataForDictionary<TKey, TValue> AsDictionary<TKey, TValue>()
|
||||
{
|
||||
return new AssetDataForDictionary<TKey, TValue>(this.Locale, this.AssetName, this.GetData<IDictionary<TKey, TValue>>(), this.GetNormalizedPath, this.ReplaceWith);
|
||||
}
|
||||
|
||||
/// <summary>Get a helper to manipulate the data as an image.</summary>
|
||||
/// <exception cref="InvalidOperationException">The content being read isn't an image.</exception>
|
||||
/// <inheritdoc />
|
||||
public IAssetDataForImage AsImage()
|
||||
{
|
||||
return new AssetDataForImage(this.Locale, this.AssetName, this.GetData<Texture2D>(), this.GetNormalizedPath, this.ReplaceWith);
|
||||
}
|
||||
|
||||
/// <summary>Get a helper to manipulate the data as a map.</summary>
|
||||
/// <exception cref="InvalidOperationException">The content being read isn't a map.</exception>
|
||||
/// <inheritdoc />
|
||||
public IAssetDataForMap AsMap()
|
||||
{
|
||||
return new AssetDataForMap(this.Locale, this.AssetName, this.GetData<Map>(), this.GetNormalizedPath, this.ReplaceWith);
|
||||
}
|
||||
|
||||
/// <summary>Get the data as a given type.</summary>
|
||||
/// <typeparam name="TData">The expected data type.</typeparam>
|
||||
/// <exception cref="InvalidCastException">The data can't be converted to <typeparamref name="TData"/>.</exception>
|
||||
/// <inheritdoc />
|
||||
public TData GetData<TData>()
|
||||
{
|
||||
if (!(this.Data is TData))
|
||||
|
||||
@@ -16,13 +16,13 @@ namespace StardewModdingAPI.Framework.Content
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The content's locale code, if the content is localized.</summary>
|
||||
/// <inheritdoc />
|
||||
public string Locale { get; }
|
||||
|
||||
/// <summary>The normalized asset name being read. The format may change between platforms; see <see cref="AssetNameEquals"/> to compare with a known path.</summary>
|
||||
/// <inheritdoc />
|
||||
public string AssetName { get; }
|
||||
|
||||
/// <summary>The content data type.</summary>
|
||||
/// <inheritdoc />
|
||||
public Type DataType { get; }
|
||||
|
||||
|
||||
@@ -42,8 +42,7 @@ namespace StardewModdingAPI.Framework.Content
|
||||
this.GetNormalizedPath = getNormalizedPath;
|
||||
}
|
||||
|
||||
/// <summary>Get whether the asset name being loaded matches a given name after normalization.</summary>
|
||||
/// <param name="path">The expected asset path, relative to the game's content folder and without the .xnb extension or locale suffix (like 'Data\ObjectInformation').</param>
|
||||
/// <inheritdoc />
|
||||
public bool AssetNameEquals(string path)
|
||||
{
|
||||
path = this.GetNormalizedPath(path);
|
||||
|
||||
@@ -9,7 +9,6 @@ using Microsoft.Xna.Framework.Content;
|
||||
using StardewModdingAPI.Framework.Content;
|
||||
using StardewModdingAPI.Framework.ContentManagers;
|
||||
using StardewModdingAPI.Framework.Reflection;
|
||||
using StardewModdingAPI.Framework.StateTracking.Comparers;
|
||||
using StardewModdingAPI.Metadata;
|
||||
using StardewModdingAPI.Toolkit.Serialization;
|
||||
using StardewModdingAPI.Toolkit.Utilities;
|
||||
@@ -96,7 +95,7 @@ namespace StardewModdingAPI.Framework
|
||||
this.ContentManagers.Add(
|
||||
this.MainContentManager = new GameContentManager("Game1.content", serviceProvider, rootDirectory, currentCulture, this, monitor, reflection, this.OnDisposing, onLoadingFirstAsset)
|
||||
);
|
||||
this.CoreAssets = new CoreAssetPropagator(this.MainContentManager.AssertAndNormalizeAssetName, reflection, monitor);
|
||||
this.CoreAssets = new CoreAssetPropagator(this.MainContentManager.AssertAndNormalizeAssetName, reflection);
|
||||
}
|
||||
|
||||
/// <summary>Get a new content manager which handles reading files from the game content folder with support for interception.</summary>
|
||||
@@ -237,7 +236,7 @@ namespace StardewModdingAPI.Framework
|
||||
{
|
||||
foreach (var entry in contentManager.InvalidateCache(predicate, dispose))
|
||||
{
|
||||
if (!removedAssets.TryGetValue(entry.Key, out Type type))
|
||||
if (!removedAssets.ContainsKey(entry.Key))
|
||||
removedAssets[entry.Key] = entry.Value.GetType();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
||||
/// <param name="onDisposing">A callback to invoke when the content manager is being disposed.</param>
|
||||
/// <param name="isNamespaced">Whether this content manager handles managed asset keys (e.g. to load assets from a mod folder).</param>
|
||||
protected BaseContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, Action<BaseContentManager> onDisposing, bool isNamespaced)
|
||||
: base(serviceProvider, rootDirectory, currentCulture)
|
||||
: base(serviceProvider, rootDirectory, currentCulture)
|
||||
{
|
||||
// init
|
||||
this.Name = name;
|
||||
|
||||
@@ -250,7 +250,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
||||
if (pixel.A == byte.MinValue || pixel.A == byte.MaxValue)
|
||||
continue; // no need to change fully transparent/opaque pixels
|
||||
|
||||
data[i] = new Color(pixel.R * pixel.A / byte.MaxValue, pixel.G * pixel.A / byte.MaxValue, pixel.B * pixel.A / byte.MaxValue, pixel.A); // slower version: Color.FromNonPremultiplied(data[i].ToVector4())
|
||||
data[i] = new Color(pixel.R * pixel.A / byte.MaxValue, pixel.G * pixel.A / byte.MaxValue, pixel.B * pixel.A / byte.MaxValue, pixel.A); // slower version: Color.FromNonPremultiplied(data[i].ToVector4())
|
||||
}
|
||||
|
||||
texture.SetData(data);
|
||||
@@ -275,12 +275,12 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
||||
/// * If the location is indoors or the desert, or the image source contains 'path' or 'object', it's loaded
|
||||
/// as-is relative to the <c>Content</c> folder.
|
||||
/// * Else it's loaded from <c>Content\Maps</c> with a seasonal prefix.
|
||||
///
|
||||
///
|
||||
/// That logic doesn't work well in our case, mainly because we have no location metadata at this point.
|
||||
/// Instead we use a more heuristic approach: check relative to the map file first, then relative to
|
||||
/// <c>Content\Maps</c>, then <c>Content</c>. If the image source filename contains a seasonal prefix, try for a
|
||||
/// seasonal variation and then an exact match.
|
||||
///
|
||||
///
|
||||
/// While that doesn't exactly match the game logic, it's close enough that it's unlikely to make a difference.
|
||||
/// </remarks>
|
||||
private void FixCustomTilesheetPaths(Map map, string relativeMapPath)
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Microsoft.Xna.Framework.Content;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
using StardewModdingAPI.Toolkit.Serialization;
|
||||
using StardewModdingAPI.Toolkit.Utilities;
|
||||
using xTile;
|
||||
|
||||
namespace StardewModdingAPI.Framework
|
||||
{
|
||||
@@ -24,13 +21,13 @@ namespace StardewModdingAPI.Framework
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The full path to the content pack's folder.</summary>
|
||||
/// <inheritdoc />
|
||||
public string DirectoryPath { get; }
|
||||
|
||||
/// <summary>The content pack's manifest.</summary>
|
||||
/// <inheritdoc />
|
||||
public IManifest Manifest { get; }
|
||||
|
||||
/// <summary>Provides translations stored in the content pack's <c>i18n</c> folder. See <see cref="IModHelper.Translation"/> for more info.</summary>
|
||||
/// <inheritdoc />
|
||||
public ITranslationHelper Translation { get; }
|
||||
|
||||
|
||||
@@ -52,8 +49,7 @@ namespace StardewModdingAPI.Framework
|
||||
this.JsonHelper = jsonHelper;
|
||||
}
|
||||
|
||||
/// <summary>Get whether a given file exists in the content pack.</summary>
|
||||
/// <param name="path">The file path to check.</param>
|
||||
/// <inheritdoc />
|
||||
public bool HasFile(string path)
|
||||
{
|
||||
this.AssertRelativePath(path, nameof(this.HasFile));
|
||||
@@ -61,11 +57,7 @@ namespace StardewModdingAPI.Framework
|
||||
return File.Exists(Path.Combine(this.DirectoryPath, path));
|
||||
}
|
||||
|
||||
/// <summary>Read a JSON file from the content pack folder.</summary>
|
||||
/// <typeparam name="TModel">The model type.</typeparam>
|
||||
/// <param name="path">The file path relative to the content directory.</param>
|
||||
/// <returns>Returns the deserialized model, or <c>null</c> if the file doesn't exist or is empty.</returns>
|
||||
/// <exception cref="InvalidOperationException">The <paramref name="path"/> is not relative or contains directory climbing (../).</exception>
|
||||
/// <inheritdoc />
|
||||
public TModel ReadJsonFile<TModel>(string path) where TModel : class
|
||||
{
|
||||
this.AssertRelativePath(path, nameof(this.ReadJsonFile));
|
||||
@@ -76,11 +68,7 @@ namespace StardewModdingAPI.Framework
|
||||
: null;
|
||||
}
|
||||
|
||||
/// <summary>Save data to a JSON file in the content pack's folder.</summary>
|
||||
/// <typeparam name="TModel">The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types.</typeparam>
|
||||
/// <param name="path">The file path relative to the mod folder.</param>
|
||||
/// <param name="data">The arbitrary data to save.</param>
|
||||
/// <exception cref="InvalidOperationException">The <paramref name="path"/> is not relative or contains directory climbing (../).</exception>
|
||||
/// <inheritdoc />
|
||||
public void WriteJsonFile<TModel>(string path, TModel data) where TModel : class
|
||||
{
|
||||
this.AssertRelativePath(path, nameof(this.WriteJsonFile));
|
||||
@@ -89,19 +77,13 @@ namespace StardewModdingAPI.Framework
|
||||
this.JsonHelper.WriteJsonFile(path, data);
|
||||
}
|
||||
|
||||
/// <summary>Load content from the content pack folder (if not already cached), and return it. When loading a <c>.png</c> file, this must be called outside the game's draw loop.</summary>
|
||||
/// <typeparam name="T">The expected data type. The main supported types are <see cref="Map"/>, <see cref="Texture2D"/>, and dictionaries; other types may be supported by the game's content pipeline.</typeparam>
|
||||
/// <param name="key">The local path to a content file relative to the content pack folder.</param>
|
||||
/// <exception cref="ArgumentException">The <paramref name="key"/> is empty or contains invalid characters.</exception>
|
||||
/// <exception cref="ContentLoadException">The content asset couldn't be loaded (e.g. because it doesn't exist).</exception>
|
||||
/// <inheritdoc />
|
||||
public T LoadAsset<T>(string key)
|
||||
{
|
||||
return this.Content.Load<T>(key, ContentSource.ModFolder);
|
||||
}
|
||||
|
||||
/// <summary>Get the underlying key in the game's content cache for an asset. This can be used to load custom map tilesheets, but should be avoided when you can use the content API instead. This does not validate whether the asset exists.</summary>
|
||||
/// <param name="key">The the local path to a content file relative to the content pack folder.</param>
|
||||
/// <exception cref="ArgumentException">The <paramref name="key"/> is empty or contains invalid characters.</exception>
|
||||
/// <inheritdoc />
|
||||
public string GetActualAssetKey(string key)
|
||||
{
|
||||
return this.Content.GetActualAssetKey(key, ContentSource.ModFolder);
|
||||
|
||||
@@ -8,16 +8,16 @@ namespace StardewModdingAPI.Framework
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The pixel position relative to the top-left corner of the in-game map, adjusted for pixel zoom.</summary>
|
||||
/// <inheritdoc />
|
||||
public Vector2 AbsolutePixels { get; }
|
||||
|
||||
/// <summary>The pixel position relative to the top-left corner of the visible screen, adjusted for pixel zoom.</summary>
|
||||
/// <inheritdoc />
|
||||
public Vector2 ScreenPixels { get; }
|
||||
|
||||
/// <summary>The tile position under the cursor relative to the top-left corner of the map.</summary>
|
||||
/// <inheritdoc />
|
||||
public Vector2 Tile { get; }
|
||||
|
||||
/// <summary>The tile position that the game considers under the cursor for purposes of clicking actions. This may be different than <see cref="Tile"/> if that's too far from the player.</summary>
|
||||
/// <inheritdoc />
|
||||
public Vector2 GrabTile { get; }
|
||||
|
||||
|
||||
@@ -37,8 +37,7 @@ namespace StardewModdingAPI.Framework
|
||||
this.GrabTile = grabTile;
|
||||
}
|
||||
|
||||
/// <summary>Get whether the current object is equal to another object of the same type.</summary>
|
||||
/// <param name="other">An object to compare with this object.</param>
|
||||
/// <inheritdoc />
|
||||
public bool Equals(ICursorPosition other)
|
||||
{
|
||||
return other != null && this.AbsolutePixels == other.AbsolutePixels;
|
||||
|
||||
@@ -12,4 +12,4 @@ namespace StardewModdingAPI.Framework
|
||||
/// <summary>The code will be removed soon. Deprecation messages should be warnings in the console.</summary>
|
||||
PendingRemoval
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,6 +107,7 @@ namespace StardewModdingAPI.Framework
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.QueuedWarnings.Clear();
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace StardewModdingAPI.Framework.Events
|
||||
internal class EventManager
|
||||
{
|
||||
/*********
|
||||
** Events (new)
|
||||
** Events
|
||||
*********/
|
||||
/****
|
||||
** Display
|
||||
|
||||
@@ -70,27 +70,33 @@ namespace StardewModdingAPI.Framework.Events
|
||||
/// <param name="mod">The mod which added the event handler.</param>
|
||||
public void Add(EventHandler<TEventArgs> handler, IModMetadata mod)
|
||||
{
|
||||
EventPriority priority = handler.Method.GetCustomAttribute<EventPriorityAttribute>()?.Priority ?? EventPriority.Normal;
|
||||
var managedHandler = new ManagedEventHandler<TEventArgs>(handler, this.RegistrationIndex++, priority, mod);
|
||||
lock (this.Handlers)
|
||||
{
|
||||
EventPriority priority = handler.Method.GetCustomAttribute<EventPriorityAttribute>()?.Priority ?? EventPriority.Normal;
|
||||
var managedHandler = new ManagedEventHandler<TEventArgs>(handler, this.RegistrationIndex++, priority, mod);
|
||||
|
||||
this.Handlers.Add(managedHandler);
|
||||
this.CachedHandlers = null;
|
||||
this.HasNewHandlers = true;
|
||||
this.Handlers.Add(managedHandler);
|
||||
this.CachedHandlers = null;
|
||||
this.HasNewHandlers = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Remove an event handler.</summary>
|
||||
/// <param name="handler">The event handler.</param>
|
||||
public void Remove(EventHandler<TEventArgs> handler)
|
||||
{
|
||||
// match C# events: if a handler is listed multiple times, remove the last one added
|
||||
for (int i = this.Handlers.Count - 1; i >= 0; i--)
|
||||
lock (this.Handlers)
|
||||
{
|
||||
if (this.Handlers[i].Handler != handler)
|
||||
continue;
|
||||
// match C# events: if a handler is listed multiple times, remove the last one added
|
||||
for (int i = this.Handlers.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (this.Handlers[i].Handler != handler)
|
||||
continue;
|
||||
|
||||
this.Handlers.RemoveAt(i);
|
||||
this.CachedHandlers = null;
|
||||
break;
|
||||
this.Handlers.RemoveAt(i);
|
||||
this.CachedHandlers = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,14 +112,17 @@ namespace StardewModdingAPI.Framework.Events
|
||||
// update cached data
|
||||
// (This is debounced here to avoid repeatedly sorting when handlers are added/removed,
|
||||
// and keeping a separate cached list allows changes during enumeration.)
|
||||
var handlers = this.CachedHandlers; // iterate local copy in case a mod adds/removes a handler while handling the event
|
||||
var handlers = this.CachedHandlers; // iterate local copy in case a mod adds/removes a handler while handling the event, which will set this field to null
|
||||
if (handlers == null)
|
||||
{
|
||||
if (this.HasNewHandlers && this.Handlers.Any(p => p.Priority != EventPriority.Normal))
|
||||
this.Handlers.Sort();
|
||||
lock (this.Handlers)
|
||||
{
|
||||
if (this.HasNewHandlers && this.Handlers.Any(p => p.Priority != EventPriority.Normal))
|
||||
this.Handlers.Sort();
|
||||
|
||||
this.CachedHandlers = handlers = this.Handlers.ToArray();
|
||||
this.HasNewHandlers = false;
|
||||
this.CachedHandlers = handlers = this.Handlers.ToArray();
|
||||
this.HasNewHandlers = false;
|
||||
}
|
||||
}
|
||||
|
||||
// raise event
|
||||
|
||||
@@ -96,7 +96,6 @@ namespace StardewModdingAPI.Framework.Events
|
||||
/// <summary>Raised after the in-game clock time changes.</summary>
|
||||
public event EventHandler<TimeChangedEventArgs> TimeChanged
|
||||
{
|
||||
|
||||
add => this.EventManager.TimeChanged.Add(value, this.Mod);
|
||||
remove => this.EventManager.TimeChanged.Remove(value);
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace StardewModdingAPI.Framework
|
||||
public GameVersion(string version)
|
||||
: base(GameVersion.GetSemanticVersionString(version), allowNonStandard: true) { }
|
||||
|
||||
/// <summary>Get a string representation of the version.</summary>
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return GameVersion.GetGameVersionString(base.ToString());
|
||||
|
||||
@@ -31,12 +31,18 @@ namespace StardewModdingAPI.Framework
|
||||
/// <summary>The metadata resolution status.</summary>
|
||||
ModMetadataStatus Status { get; }
|
||||
|
||||
/// <summary>The reason the mod failed to load, if applicable.</summary>
|
||||
ModFailReason? FailReason { get; }
|
||||
|
||||
/// <summary>Indicates non-error issues with the mod.</summary>
|
||||
ModWarning Warnings { get; }
|
||||
|
||||
/// <summary>The reason the metadata is invalid, if any.</summary>
|
||||
string Error { get; }
|
||||
|
||||
/// <summary>A detailed technical message for <see cref="Error"/>, if any.</summary>
|
||||
public string ErrorDetails { get; }
|
||||
|
||||
/// <summary>Whether the mod folder should be ignored. This is <c>true</c> if it was found within a folder whose name starts with a dot.</summary>
|
||||
bool IsIgnored { get; }
|
||||
|
||||
@@ -62,11 +68,17 @@ namespace StardewModdingAPI.Framework
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Set the mod status to <see cref="ModMetadataStatus.Found"/>.</summary>
|
||||
/// <returns>Return the instance for chaining.</returns>
|
||||
IModMetadata SetStatusFound();
|
||||
|
||||
/// <summary>Set the mod status.</summary>
|
||||
/// <param name="status">The metadata resolution status.</param>
|
||||
/// <param name="reason">The reason a mod could not be loaded.</param>
|
||||
/// <param name="error">The reason the metadata is invalid, if any.</param>
|
||||
/// <param name="errorDetails">A detailed technical message, if any.</param>
|
||||
/// <returns>Return the instance for chaining.</returns>
|
||||
IModMetadata SetStatus(ModMetadataStatus status, string error = null);
|
||||
IModMetadata SetStatus(ModMetadataStatus status, ModFailReason reason, string error, string errorDetails = null);
|
||||
|
||||
/// <summary>Set a warning flag for the mod.</summary>
|
||||
/// <param name="warning">The warning to set.</param>
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace StardewModdingAPI.Framework.Input
|
||||
private GamePadState? State;
|
||||
|
||||
/// <summary>The current button states.</summary>
|
||||
private IDictionary<SButton, ButtonState> ButtonStates;
|
||||
private readonly IDictionary<SButton, ButtonState> ButtonStates;
|
||||
|
||||
/// <summary>The left trigger value.</summary>
|
||||
private float LeftTrigger;
|
||||
@@ -39,33 +39,26 @@ namespace StardewModdingAPI.Framework.Input
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>Whether the gamepad is currently connected.</summary>
|
||||
public bool IsConnected { get; private set; }
|
||||
public bool IsConnected { get; }
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="state">The initial state, or <c>null</c> to get the latest state.</param>
|
||||
public GamePadStateBuilder(GamePadState? state = null)
|
||||
/// <param name="state">The initial state.</param>
|
||||
public GamePadStateBuilder(GamePadState state)
|
||||
{
|
||||
this.Reset(state);
|
||||
}
|
||||
|
||||
/// <summary>Reset the tracked state.</summary>
|
||||
/// <param name="state">The state from which to reset, or <c>null</c> to get the latest state.</param>
|
||||
public GamePadStateBuilder Reset(GamePadState? state = null)
|
||||
{
|
||||
this.State = state ??= GamePad.GetState(PlayerIndex.One);
|
||||
this.IsConnected = state.Value.IsConnected;
|
||||
this.State = state;
|
||||
this.IsConnected = state.IsConnected;
|
||||
|
||||
if (!this.IsConnected)
|
||||
return this;
|
||||
return;
|
||||
|
||||
GamePadDPad pad = state.Value.DPad;
|
||||
GamePadButtons buttons = state.Value.Buttons;
|
||||
GamePadTriggers triggers = state.Value.Triggers;
|
||||
GamePadThumbSticks sticks = state.Value.ThumbSticks;
|
||||
GamePadDPad pad = state.DPad;
|
||||
GamePadButtons buttons = state.Buttons;
|
||||
GamePadTriggers triggers = state.Triggers;
|
||||
GamePadThumbSticks sticks = state.ThumbSticks;
|
||||
this.ButtonStates = new Dictionary<SButton, ButtonState>
|
||||
{
|
||||
[SButton.DPadUp] = pad.Up,
|
||||
@@ -89,8 +82,6 @@ namespace StardewModdingAPI.Framework.Input
|
||||
this.RightTrigger = triggers.Right;
|
||||
this.LeftStickPos = sticks.Left;
|
||||
this.RightStickPos = sticks.Right;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>Override the states for a set of buttons.</summary>
|
||||
|
||||
@@ -12,10 +12,6 @@ namespace StardewModdingAPI.Framework.Input
|
||||
/*********
|
||||
** Methods
|
||||
*********/
|
||||
/// <summary>Reset the tracked state.</summary>
|
||||
/// <param name="state">The state from which to reset, or <c>null</c> to get the latest state.</param>
|
||||
THandler Reset(TState? state = null);
|
||||
|
||||
/// <summary>Override the states for a set of buttons.</summary>
|
||||
/// <param name="overrides">The button state overrides.</param>
|
||||
THandler OverrideButtons(IDictionary<SButton, SButtonState> overrides);
|
||||
|
||||
@@ -21,23 +21,14 @@ namespace StardewModdingAPI.Framework.Input
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="state">The initial state, or <c>null</c> to get the latest state.</param>
|
||||
public KeyboardStateBuilder(KeyboardState? state = null)
|
||||
/// <param name="state">The initial state.</param>
|
||||
public KeyboardStateBuilder(KeyboardState state)
|
||||
{
|
||||
this.Reset(state);
|
||||
}
|
||||
|
||||
/// <summary>Reset the tracked state.</summary>
|
||||
/// <param name="state">The state from which to reset, or <c>null</c> to get the latest state.</param>
|
||||
public KeyboardStateBuilder Reset(KeyboardState? state = null)
|
||||
{
|
||||
this.State = state ??= Keyboard.GetState();
|
||||
this.State = state;
|
||||
|
||||
this.PressedButtons.Clear();
|
||||
foreach (var button in state.Value.GetPressedKeys())
|
||||
foreach (var button in state.GetPressedKeys())
|
||||
this.PressedButtons.Add(button);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>Override the states for a set of buttons.</summary>
|
||||
|
||||
@@ -13,51 +13,42 @@ namespace StardewModdingAPI.Framework.Input
|
||||
private MouseState? State;
|
||||
|
||||
/// <summary>The current button states.</summary>
|
||||
private IDictionary<SButton, ButtonState> ButtonStates;
|
||||
private readonly IDictionary<SButton, ButtonState> ButtonStates;
|
||||
|
||||
/// <summary>The mouse wheel scroll value.</summary>
|
||||
private int ScrollWheelValue;
|
||||
private readonly int ScrollWheelValue;
|
||||
|
||||
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The X cursor position.</summary>
|
||||
public int X { get; private set; }
|
||||
public int X { get; }
|
||||
|
||||
/// <summary>The Y cursor position.</summary>
|
||||
public int Y { get; private set; }
|
||||
public int Y { get; }
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="state">The initial state, or <c>null</c> to get the latest state.</param>
|
||||
public MouseStateBuilder(MouseState? state = null)
|
||||
/// <param name="state">The initial state.</param>
|
||||
public MouseStateBuilder(MouseState state)
|
||||
{
|
||||
this.Reset(state);
|
||||
}
|
||||
|
||||
/// <summary>Reset the tracked state.</summary>
|
||||
/// <param name="state">The state from which to reset, or <c>null</c> to get the latest state.</param>
|
||||
public MouseStateBuilder Reset(MouseState? state = null)
|
||||
{
|
||||
this.State = state ??= Mouse.GetState();
|
||||
this.State = state;
|
||||
|
||||
this.ButtonStates = new Dictionary<SButton, ButtonState>
|
||||
{
|
||||
[SButton.MouseLeft] = state.Value.LeftButton,
|
||||
[SButton.MouseMiddle] = state.Value.MiddleButton,
|
||||
[SButton.MouseRight] = state.Value.RightButton,
|
||||
[SButton.MouseX1] = state.Value.XButton1,
|
||||
[SButton.MouseX2] = state.Value.XButton2
|
||||
[SButton.MouseLeft] = state.LeftButton,
|
||||
[SButton.MouseMiddle] = state.MiddleButton,
|
||||
[SButton.MouseRight] = state.RightButton,
|
||||
[SButton.MouseX1] = state.XButton1,
|
||||
[SButton.MouseX2] = state.XButton2
|
||||
};
|
||||
this.X = state.Value.X;
|
||||
this.Y = state.Value.Y;
|
||||
this.ScrollWheelValue = state.Value.ScrollWheelValue;
|
||||
|
||||
return this;
|
||||
this.X = state.X;
|
||||
this.Y = state.Y;
|
||||
this.ScrollWheelValue = state.ScrollWheelValue;
|
||||
}
|
||||
|
||||
/// <summary>Override the states for a set of buttons.</summary>
|
||||
|
||||
@@ -29,21 +29,24 @@ namespace StardewModdingAPI.Framework.Input
|
||||
/// <summary>Whether there are new overrides in <see cref="CustomPressedKeys"/> or <see cref="CustomReleasedKeys"/> that haven't been applied to the previous state.</summary>
|
||||
private bool HasNewOverrides;
|
||||
|
||||
/// <summary>The game tick when the input state was last updated.</summary>
|
||||
private uint? LastUpdateTick;
|
||||
|
||||
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The controller state as of the last update.</summary>
|
||||
public GamePadState LastController { get; private set; }
|
||||
/// <summary>The controller state as of the last update, with overrides applied.</summary>
|
||||
public GamePadState ControllerState { get; private set; }
|
||||
|
||||
/// <summary>The keyboard state as of the last update.</summary>
|
||||
public KeyboardState LastKeyboard { get; private set; }
|
||||
/// <summary>The keyboard state as of the last update, with overrides applied.</summary>
|
||||
public KeyboardState KeyboardState { get; private set; }
|
||||
|
||||
/// <summary>The mouse state as of the last update.</summary>
|
||||
public MouseState LastMouse { get; private set; }
|
||||
/// <summary>The mouse state as of the last update, with overrides applied.</summary>
|
||||
public MouseState MouseState { get; private set; }
|
||||
|
||||
/// <summary>The buttons which were pressed, held, or released as of the last update.</summary>
|
||||
public IDictionary<SButton, SButtonState> LastButtonStates { get; private set; } = new Dictionary<SButton, SButtonState>();
|
||||
public IDictionary<SButton, SButtonState> ButtonStates { get; private set; } = new Dictionary<SButton, SButtonState>();
|
||||
|
||||
/// <summary>The cursor position on the screen adjusted for the zoom level.</summary>
|
||||
public ICursorPosition CursorPosition => this.CursorPositionImpl;
|
||||
@@ -52,54 +55,26 @@ namespace StardewModdingAPI.Framework.Input
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Get a copy of the current state.</summary>
|
||||
public SInputState Clone()
|
||||
/// <summary>Update the current button states for the given tick. This does nothing if the input has already been updated for this tick (e.g. because SMAPI updated it before the game update).</summary>
|
||||
public override void Update()
|
||||
{
|
||||
return new SInputState
|
||||
{
|
||||
LastButtonStates = this.LastButtonStates,
|
||||
LastController = this.LastController,
|
||||
LastKeyboard = this.LastKeyboard,
|
||||
LastMouse = this.LastMouse,
|
||||
CursorPositionImpl = this.CursorPositionImpl
|
||||
};
|
||||
}
|
||||
// skip if already updated
|
||||
if (this.LastUpdateTick == SCore.TicksElapsed)
|
||||
return;
|
||||
this.LastUpdateTick = SCore.TicksElapsed;
|
||||
|
||||
/// <summary>Override the state for a button.</summary>
|
||||
/// <param name="button">The button to override.</param>
|
||||
/// <param name="setDown">Whether to mark it pressed; else mark it released.</param>
|
||||
public void OverrideButton(SButton button, bool setDown)
|
||||
{
|
||||
bool changed = setDown
|
||||
? this.CustomPressedKeys.Add(button) | this.CustomReleasedKeys.Remove(button)
|
||||
: this.CustomPressedKeys.Remove(button) | this.CustomReleasedKeys.Add(button);
|
||||
// update base state
|
||||
base.Update();
|
||||
|
||||
if (changed)
|
||||
this.HasNewOverrides = true;
|
||||
}
|
||||
|
||||
/// <summary>Get whether a mod has indicated the key was already handled, so the game shouldn't handle it.</summary>
|
||||
/// <param name="button">The button to check.</param>
|
||||
public bool IsSuppressed(SButton button)
|
||||
{
|
||||
return this.CustomReleasedKeys.Contains(button);
|
||||
}
|
||||
|
||||
/// <summary>This method is called by the game, and does nothing since SMAPI will already have updated by that point.</summary>
|
||||
[Obsolete("This method should only be called by the game itself.")]
|
||||
public override void Update() { }
|
||||
|
||||
/// <summary>Update the current button states for the given tick.</summary>
|
||||
public void TrueUpdate()
|
||||
{
|
||||
// update SMAPI extended data
|
||||
try
|
||||
{
|
||||
float zoomMultiplier = (1f / Game1.options.zoomLevel);
|
||||
|
||||
// get real values
|
||||
var controller = new GamePadStateBuilder();
|
||||
var keyboard = new KeyboardStateBuilder();
|
||||
var mouse = new MouseStateBuilder();
|
||||
var controller = new GamePadStateBuilder(base.GetGamePadState());
|
||||
var keyboard = new KeyboardStateBuilder(base.GetKeyboardState());
|
||||
var mouse = new MouseStateBuilder(base.GetMouseState());
|
||||
Vector2 cursorAbsolutePos = new Vector2((mouse.X * zoomMultiplier) + Game1.viewport.X, (mouse.Y * zoomMultiplier) + Game1.viewport.Y);
|
||||
Vector2? playerTilePos = Context.IsPlayerFree ? Game1.player.getTileLocation() : (Vector2?)null;
|
||||
HashSet<SButton> reallyDown = new HashSet<SButton>(this.GetPressedButtons(keyboard, mouse, controller));
|
||||
@@ -124,18 +99,18 @@ namespace StardewModdingAPI.Framework.Input
|
||||
var pressedButtons = hasOverrides
|
||||
? new HashSet<SButton>(this.GetPressedButtons(keyboard, mouse, controller))
|
||||
: reallyDown;
|
||||
var activeButtons = this.DeriveStates(this.LastButtonStates, pressedButtons);
|
||||
var activeButtons = this.DeriveStates(this.ButtonStates, pressedButtons);
|
||||
|
||||
// update
|
||||
this.HasNewOverrides = false;
|
||||
this.LastController = controller.GetState();
|
||||
this.LastKeyboard = keyboard.GetState();
|
||||
this.LastMouse = mouse.GetState();
|
||||
this.LastButtonStates = activeButtons;
|
||||
this.ControllerState = controller.GetState();
|
||||
this.KeyboardState = keyboard.GetState();
|
||||
this.MouseState = mouse.GetState();
|
||||
this.ButtonStates = activeButtons;
|
||||
if (cursorAbsolutePos != this.CursorPositionImpl?.AbsolutePixels || playerTilePos != this.LastPlayerTile)
|
||||
{
|
||||
this.LastPlayerTile = playerTilePos;
|
||||
this.CursorPositionImpl = this.GetCursorPosition(this.LastMouse, cursorAbsolutePos, zoomMultiplier);
|
||||
this.CursorPositionImpl = this.GetCursorPosition(this.MouseState, cursorAbsolutePos, zoomMultiplier);
|
||||
}
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
@@ -144,53 +119,67 @@ namespace StardewModdingAPI.Framework.Input
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Get the gamepad state visible to the game.</summary>
|
||||
public override GamePadState GetGamePadState()
|
||||
{
|
||||
return this.ControllerState;
|
||||
}
|
||||
|
||||
/// <summary>Get the keyboard state visible to the game.</summary>
|
||||
public override KeyboardState GetKeyboardState()
|
||||
{
|
||||
return this.KeyboardState;
|
||||
}
|
||||
|
||||
/// <summary>Get the keyboard state visible to the game.</summary>
|
||||
public override MouseState GetMouseState()
|
||||
{
|
||||
return this.MouseState;
|
||||
}
|
||||
|
||||
/// <summary>Override the state for a button.</summary>
|
||||
/// <param name="button">The button to override.</param>
|
||||
/// <param name="setDown">Whether to mark it pressed; else mark it released.</param>
|
||||
public void OverrideButton(SButton button, bool setDown)
|
||||
{
|
||||
bool changed = setDown
|
||||
? this.CustomPressedKeys.Add(button) | this.CustomReleasedKeys.Remove(button)
|
||||
: this.CustomPressedKeys.Remove(button) | this.CustomReleasedKeys.Add(button);
|
||||
|
||||
if (changed)
|
||||
this.HasNewOverrides = true;
|
||||
}
|
||||
|
||||
/// <summary>Get whether a mod has indicated the key was already handled, so the game shouldn't handle it.</summary>
|
||||
/// <param name="button">The button to check.</param>
|
||||
public bool IsSuppressed(SButton button)
|
||||
{
|
||||
return this.CustomReleasedKeys.Contains(button);
|
||||
}
|
||||
|
||||
/// <summary>Apply input overrides to the current state.</summary>
|
||||
public void ApplyOverrides()
|
||||
{
|
||||
if (this.HasNewOverrides)
|
||||
{
|
||||
var controller = new GamePadStateBuilder(this.LastController);
|
||||
var keyboard = new KeyboardStateBuilder(this.LastKeyboard);
|
||||
var mouse = new MouseStateBuilder(this.LastMouse);
|
||||
var controller = new GamePadStateBuilder(this.ControllerState);
|
||||
var keyboard = new KeyboardStateBuilder(this.KeyboardState);
|
||||
var mouse = new MouseStateBuilder(this.MouseState);
|
||||
|
||||
if (this.ApplyOverrides(pressed: this.CustomPressedKeys, released: this.CustomReleasedKeys, controller, keyboard, mouse))
|
||||
{
|
||||
this.LastController = controller.GetState();
|
||||
this.LastKeyboard = keyboard.GetState();
|
||||
this.LastMouse = mouse.GetState();
|
||||
this.ControllerState = controller.GetState();
|
||||
this.KeyboardState = keyboard.GetState();
|
||||
this.MouseState = mouse.GetState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Get the gamepad state visible to the game.</summary>
|
||||
[Obsolete("This method should only be called by the game itself.")]
|
||||
public override GamePadState GetGamePadState()
|
||||
{
|
||||
if (Game1.options.gamepadMode == Options.GamepadModes.ForceOff)
|
||||
return new GamePadState();
|
||||
|
||||
return this.LastController;
|
||||
}
|
||||
|
||||
/// <summary>Get the keyboard state visible to the game.</summary>
|
||||
[Obsolete("This method should only be called by the game itself.")]
|
||||
public override KeyboardState GetKeyboardState()
|
||||
{
|
||||
return this.LastKeyboard;
|
||||
}
|
||||
|
||||
/// <summary>Get the keyboard state visible to the game.</summary>
|
||||
[Obsolete("This method should only be called by the game itself.")]
|
||||
public override MouseState GetMouseState()
|
||||
{
|
||||
return this.LastMouse;
|
||||
}
|
||||
|
||||
/// <summary>Get whether a given button was pressed or held.</summary>
|
||||
/// <param name="button">The button to check.</param>
|
||||
public bool IsDown(SButton button)
|
||||
{
|
||||
return this.GetState(this.LastButtonStates, button).IsDown();
|
||||
return this.GetState(this.ButtonStates, button).IsDown();
|
||||
}
|
||||
|
||||
/// <summary>Get whether any of the given buttons were pressed or held.</summary>
|
||||
@@ -204,7 +193,7 @@ namespace StardewModdingAPI.Framework.Input
|
||||
/// <param name="button">The button to check.</param>
|
||||
public SButtonState GetState(SButton button)
|
||||
{
|
||||
return this.GetState(this.LastButtonStates, button);
|
||||
return this.GetState(this.ButtonStates, button);
|
||||
}
|
||||
|
||||
|
||||
@@ -305,7 +294,9 @@ namespace StardewModdingAPI.Framework.Input
|
||||
/// <param name="button">The button to check.</param>
|
||||
private SButtonState GetState(IDictionary<SButton, SButtonState> activeButtons, SButton button)
|
||||
{
|
||||
return activeButtons.TryGetValue(button, out SButtonState state) ? state : SButtonState.None;
|
||||
return activeButtons.TryGetValue(button, out SButtonState state)
|
||||
? state
|
||||
: SButtonState.None;
|
||||
}
|
||||
|
||||
/// <summary>Get the buttons pressed in the given stats.</summary>
|
||||
|
||||
@@ -164,9 +164,9 @@ namespace StardewModdingAPI.Framework
|
||||
// get field name
|
||||
const string fieldName =
|
||||
#if SMAPI_FOR_WINDOWS
|
||||
"inBeginEndPair";
|
||||
"inBeginEndPair";
|
||||
#else
|
||||
"_beginCalled";
|
||||
"_beginCalled";
|
||||
#endif
|
||||
|
||||
// get result
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace StardewModdingAPI.Framework.Logging
|
||||
{
|
||||
/// <summary>Manages console output interception.</summary>
|
||||
internal class ConsoleInterceptionManager : IDisposable
|
||||
{
|
||||
/*********
|
||||
** Fields
|
||||
*********/
|
||||
/// <summary>The intercepting console writer.</summary>
|
||||
private readonly InterceptingTextWriter Output;
|
||||
|
||||
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The event raised when a message is written to the console directly.</summary>
|
||||
public event Action<string> OnMessageIntercepted;
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
public ConsoleInterceptionManager()
|
||||
{
|
||||
// redirect output through interceptor
|
||||
this.Output = new InterceptingTextWriter(Console.Out);
|
||||
this.Output.OnMessageIntercepted += line => this.OnMessageIntercepted?.Invoke(line);
|
||||
Console.SetOut(this.Output);
|
||||
}
|
||||
|
||||
/// <summary>Get an exclusive lock and write to the console output without interception.</summary>
|
||||
/// <param name="action">The action to perform within the exclusive write block.</param>
|
||||
public void ExclusiveWriteWithoutInterception(Action action)
|
||||
{
|
||||
lock (Console.Out)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.Output.ShouldIntercept = false;
|
||||
action();
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.Output.ShouldIntercept = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Release all resources.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Console.SetOut(this.Output.Out);
|
||||
this.Output.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,18 +7,22 @@ namespace StardewModdingAPI.Framework.Logging
|
||||
/// <summary>A text writer which allows intercepting output.</summary>
|
||||
internal class InterceptingTextWriter : TextWriter
|
||||
{
|
||||
/*********
|
||||
** Fields
|
||||
*********/
|
||||
/// <summary>Prefixing a message with this character indicates that the console interceptor should write the string without intercepting it. (The character itself is not written.)</summary>
|
||||
private readonly char IgnoreChar;
|
||||
|
||||
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The underlying console output.</summary>
|
||||
public TextWriter Out { get; }
|
||||
|
||||
/// <summary>The character encoding in which the output is written.</summary>
|
||||
/// <inheritdoc />
|
||||
public override Encoding Encoding => this.Out.Encoding;
|
||||
|
||||
/// <summary>Whether to intercept console output.</summary>
|
||||
public bool ShouldIntercept { get; set; }
|
||||
|
||||
/// <summary>The event raised when a message is written to the console directly.</summary>
|
||||
public event Action<string> OnMessageIntercepted;
|
||||
|
||||
@@ -28,36 +32,53 @@ namespace StardewModdingAPI.Framework.Logging
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="output">The underlying output writer.</param>
|
||||
public InterceptingTextWriter(TextWriter output)
|
||||
/// <param name="ignoreChar">Prefixing a message with this character indicates that the console interceptor should write the string without intercepting it. (The character itself is not written.)</param>
|
||||
public InterceptingTextWriter(TextWriter output, char ignoreChar)
|
||||
{
|
||||
this.Out = output;
|
||||
this.IgnoreChar = ignoreChar;
|
||||
}
|
||||
|
||||
/// <summary>Writes a subarray of characters to the text string or stream.</summary>
|
||||
/// <param name="buffer">The character array to write data from.</param>
|
||||
/// <param name="index">The character position in the buffer at which to start retrieving data.</param>
|
||||
/// <param name="count">The number of characters to write.</param>
|
||||
/// <inheritdoc />
|
||||
public override void Write(char[] buffer, int index, int count)
|
||||
{
|
||||
if (this.ShouldIntercept)
|
||||
this.OnMessageIntercepted?.Invoke(new string(buffer, index, count).TrimEnd('\r', '\n'));
|
||||
else
|
||||
if (buffer.Length == 0)
|
||||
this.Out.Write(buffer, index, count);
|
||||
else if (buffer[0] == this.IgnoreChar)
|
||||
this.Out.Write(buffer, index + 1, count - 1);
|
||||
else if (this.IsEmptyOrNewline(buffer))
|
||||
this.Out.Write(buffer, index, count);
|
||||
else
|
||||
this.OnMessageIntercepted?.Invoke(new string(buffer, index, count).TrimEnd('\r', '\n'));
|
||||
}
|
||||
|
||||
/// <summary>Writes a character to the text string or stream.</summary>
|
||||
/// <param name="ch">The character to write to the text stream.</param>
|
||||
/// <remarks>Console log messages from the game should be caught by <see cref="Write(char[],int,int)"/>. This method passes through anything that bypasses that method for some reason, since it's better to show it to users than hide it from everyone.</remarks>
|
||||
/// <inheritdoc />
|
||||
public override void Write(char ch)
|
||||
{
|
||||
this.Out.Write(ch);
|
||||
}
|
||||
|
||||
/// <summary>Releases the unmanaged resources used by the <see cref="T:System.IO.TextWriter" /> and optionally releases the managed resources.</summary>
|
||||
/// <param name="disposing">true to release both managed and unmanaged resources; false to release only unmanaged resources.</param>
|
||||
/// <inheritdoc />
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
this.OnMessageIntercepted = null;
|
||||
}
|
||||
|
||||
|
||||
/*********
|
||||
** Private methods
|
||||
*********/
|
||||
/// <summary>Get whether a buffer represents a line break.</summary>
|
||||
/// <param name="buffer">The buffer to check.</param>
|
||||
private bool IsEmptyOrNewline(char[] buffer)
|
||||
{
|
||||
foreach (char ch in buffer)
|
||||
{
|
||||
if (ch != '\n' && ch != '\r')
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,600 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using StardewModdingAPI.Framework.Commands;
|
||||
using StardewModdingAPI.Framework.ModLoading;
|
||||
using StardewModdingAPI.Internal.ConsoleWriting;
|
||||
using StardewModdingAPI.Toolkit.Framework.ModData;
|
||||
using StardewModdingAPI.Toolkit.Utilities;
|
||||
|
||||
namespace StardewModdingAPI.Framework.Logging
|
||||
{
|
||||
/// <summary>Manages the SMAPI console window and log file.</summary>
|
||||
internal class LogManager : IDisposable
|
||||
{
|
||||
/*********
|
||||
** Fields
|
||||
*********/
|
||||
/// <summary>The log file to which to write messages.</summary>
|
||||
private readonly LogFileManager LogFile;
|
||||
|
||||
/// <summary>Prefixing a low-level message with this character indicates that the console interceptor should write the string without intercepting it. (The character itself is not written.)</summary>
|
||||
private readonly char IgnoreChar = '\u200B';
|
||||
|
||||
/// <summary>Get a named monitor instance.</summary>
|
||||
private readonly Func<string, Monitor> GetMonitorImpl;
|
||||
|
||||
/// <summary>Regex patterns which match console non-error messages to suppress from the console and log.</summary>
|
||||
private readonly Regex[] SuppressConsolePatterns =
|
||||
{
|
||||
new Regex(@"^TextBox\.Selected is now '(?:True|False)'\.$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||
new Regex(@"^(?:FRUIT )?TREE: IsClient:(?:True|False) randomOutput: \d+$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||
new Regex(@"^loadPreferences\(\); begin", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||
new Regex(@"^savePreferences\(\); async=", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||
new Regex(@"^DebugOutput:\s+(?:added CLOUD|added cricket|dismount tile|Ping|playerPos)", RegexOptions.Compiled | RegexOptions.CultureInvariant)
|
||||
};
|
||||
|
||||
/// <summary>Regex patterns which match console messages to show a more friendly error for.</summary>
|
||||
private readonly ReplaceLogPattern[] ReplaceConsolePatterns =
|
||||
{
|
||||
// Steam not loaded
|
||||
new ReplaceLogPattern(
|
||||
search: new Regex(@"^System\.InvalidOperationException: Steamworks is not initialized\.[\s\S]+$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||
replacement:
|
||||
#if SMAPI_FOR_WINDOWS
|
||||
"Oops! Steam achievements won't work because Steam isn't loaded. You can launch the game through Steam to fix that (see 'Part 2: Configure Steam' in the install guide for more info: https://smapi.io/install).",
|
||||
#else
|
||||
"Oops! Steam achievements won't work because Steam isn't loaded. You can launch the game through Steam to fix that.",
|
||||
#endif
|
||||
logLevel: LogLevel.Error
|
||||
),
|
||||
|
||||
// save file not found error
|
||||
new ReplaceLogPattern(
|
||||
search: new Regex(@"^System\.IO\.FileNotFoundException: [^\n]+\n[^:]+: '[^\n]+[/\\]Saves[/\\]([^'\r\n]+)[/\\]([^'\r\n]+)'[\s\S]+LoadGameMenu\.FindSaveGames[\s\S]+$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||
replacement: "The game can't find the '$2' file for your '$1' save. See https://stardewvalleywiki.com/Saves#Troubleshooting for help.",
|
||||
logLevel: LogLevel.Error
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The core logger and monitor for SMAPI.</summary>
|
||||
public Monitor Monitor { get; }
|
||||
|
||||
/// <summary>The core logger and monitor on behalf of the game.</summary>
|
||||
public Monitor MonitorForGame { get; }
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/****
|
||||
** Initialization
|
||||
****/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="logPath">The log file path to write.</param>
|
||||
/// <param name="colorConfig">The colors to use for text written to the SMAPI console.</param>
|
||||
/// <param name="writeToConsole">Whether to output log messages to the console.</param>
|
||||
/// <param name="isVerbose">Whether verbose logging is enabled. This enables more detailed diagnostic messages than are normally needed.</param>
|
||||
/// <param name="isDeveloperMode">Whether to enable full console output for developers.</param>
|
||||
public LogManager(string logPath, ColorSchemeConfig colorConfig, bool writeToConsole, bool isVerbose, bool isDeveloperMode)
|
||||
{
|
||||
// init construction logic
|
||||
this.GetMonitorImpl = name => new Monitor(name, this.IgnoreChar, this.LogFile, colorConfig, isVerbose)
|
||||
{
|
||||
WriteToConsole = writeToConsole,
|
||||
ShowTraceInConsole = isDeveloperMode,
|
||||
ShowFullStampInConsole = isDeveloperMode
|
||||
};
|
||||
|
||||
// init fields
|
||||
this.LogFile = new LogFileManager(logPath);
|
||||
this.Monitor = this.GetMonitor("SMAPI");
|
||||
this.MonitorForGame = this.GetMonitor("game");
|
||||
|
||||
// redirect direct console output
|
||||
var output = new InterceptingTextWriter(Console.Out, this.IgnoreChar);
|
||||
if (writeToConsole)
|
||||
output.OnMessageIntercepted += message => this.HandleConsoleMessage(this.MonitorForGame, message);
|
||||
Console.SetOut(output);
|
||||
}
|
||||
|
||||
/// <summary>Get a monitor instance derived from SMAPI's current settings.</summary>
|
||||
/// <param name="name">The name of the module which will log messages with this instance.</param>
|
||||
public Monitor GetMonitor(string name)
|
||||
{
|
||||
return this.GetMonitorImpl(name);
|
||||
}
|
||||
|
||||
/// <summary>Set the title of the SMAPI console window.</summary>
|
||||
/// <param name="title">The new window title.</param>
|
||||
public void SetConsoleTitle(string title)
|
||||
{
|
||||
Console.Title = title;
|
||||
}
|
||||
|
||||
/****
|
||||
** Console input
|
||||
****/
|
||||
/// <summary>Run a loop handling console input.</summary>
|
||||
[SuppressMessage("ReSharper", "FunctionNeverReturns", Justification = "The thread is aborted when the game exits.")]
|
||||
public void RunConsoleInputLoop(CommandManager commandManager, Action reloadTranslations, Action<string> handleInput, Func<bool> continueWhile)
|
||||
{
|
||||
// prepare console
|
||||
this.Monitor.Log("Type 'help' for help, or 'help <cmd>' for a command's usage", LogLevel.Info);
|
||||
commandManager
|
||||
.Add(new HelpCommand(commandManager), this.Monitor)
|
||||
.Add(new HarmonySummaryCommand(), this.Monitor)
|
||||
.Add(new ReloadI18nCommand(reloadTranslations), this.Monitor);
|
||||
|
||||
// start handling command line input
|
||||
Thread inputThread = new Thread(() =>
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
// get input
|
||||
string input = Console.ReadLine();
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
continue;
|
||||
|
||||
// handle command
|
||||
this.Monitor.LogUserInput(input);
|
||||
handleInput(input);
|
||||
}
|
||||
});
|
||||
inputThread.Start();
|
||||
|
||||
// keep console thread alive while the game is running
|
||||
while (continueWhile())
|
||||
Thread.Sleep(1000 / 10);
|
||||
if (inputThread.ThreadState == ThreadState.Running)
|
||||
inputThread.Abort();
|
||||
}
|
||||
|
||||
/// <summary>Show a 'press any key to exit' message, and exit when they press a key.</summary>
|
||||
public void PressAnyKeyToExit()
|
||||
{
|
||||
this.Monitor.Log("Game has ended. Press any key to exit.", LogLevel.Info);
|
||||
this.PressAnyKeyToExit(showMessage: false);
|
||||
}
|
||||
|
||||
/// <summary>Show a 'press any key to exit' message, and exit when they press a key.</summary>
|
||||
/// <param name="showMessage">Whether to print a 'press any key to exit' message to the console.</param>
|
||||
public void PressAnyKeyToExit(bool showMessage)
|
||||
{
|
||||
if (showMessage)
|
||||
this.Monitor.Log("Game has ended. Press any key to exit.");
|
||||
Thread.Sleep(100);
|
||||
Console.ReadKey();
|
||||
Environment.Exit(0);
|
||||
}
|
||||
|
||||
/****
|
||||
** Crash/update handling
|
||||
****/
|
||||
/// <summary>Create a crash marker and duplicate the log into the crash log.</summary>
|
||||
public void WriteCrashLog()
|
||||
{
|
||||
try
|
||||
{
|
||||
File.WriteAllText(Constants.FatalCrashMarker, string.Empty);
|
||||
File.Copy(this.LogFile.Path, Constants.FatalCrashLog, overwrite: true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Monitor.Log($"SMAPI failed trying to track the crash details: {ex.GetLogSummary()}", LogLevel.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Write an update alert marker file.</summary>
|
||||
/// <param name="version">The new version found.</param>
|
||||
public void WriteUpdateMarker(string version)
|
||||
{
|
||||
File.WriteAllText(Constants.UpdateMarker, version);
|
||||
}
|
||||
|
||||
/// <summary>Check whether SMAPI crashed or detected an update during the last session, and display them in the SMAPI console.</summary>
|
||||
public void HandleMarkerFiles()
|
||||
{
|
||||
// show update alert
|
||||
if (File.Exists(Constants.UpdateMarker))
|
||||
{
|
||||
string rawUpdateFound = File.ReadAllText(Constants.UpdateMarker);
|
||||
if (SemanticVersion.TryParse(rawUpdateFound, out ISemanticVersion updateFound))
|
||||
{
|
||||
if (Constants.ApiVersion.IsPrerelease() && updateFound.IsNewerThan(Constants.ApiVersion))
|
||||
{
|
||||
this.Monitor.Log("A new version of SMAPI was detected last time you played.", LogLevel.Error);
|
||||
this.Monitor.Log($"You can update to {updateFound}: https://smapi.io.", LogLevel.Error);
|
||||
this.Monitor.Log("Press any key to continue playing anyway. (This only appears when using a SMAPI beta.)", LogLevel.Info);
|
||||
Console.ReadKey();
|
||||
}
|
||||
}
|
||||
File.Delete(Constants.UpdateMarker);
|
||||
}
|
||||
|
||||
// show details if game crashed during last session
|
||||
if (File.Exists(Constants.FatalCrashMarker))
|
||||
{
|
||||
this.Monitor.Log("The game crashed last time you played. If it happens repeatedly, see 'get help' on https://smapi.io.", LogLevel.Error);
|
||||
this.Monitor.Log("If you ask for help, make sure to share your SMAPI log: https://smapi.io/log.", LogLevel.Error);
|
||||
this.Monitor.Log("Press any key to delete the crash data and continue playing.", LogLevel.Info);
|
||||
Console.ReadKey();
|
||||
File.Delete(Constants.FatalCrashLog);
|
||||
File.Delete(Constants.FatalCrashMarker);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Log a fatal exception which prevents SMAPI from launching.</summary>
|
||||
/// <param name="exception">The exception details.</param>
|
||||
public void LogFatalLaunchError(Exception exception)
|
||||
{
|
||||
switch (exception)
|
||||
{
|
||||
// audio crash
|
||||
case InvalidOperationException ex when ex.Source == "Microsoft.Xna.Framework.Xact" && ex.StackTrace.Contains("Microsoft.Xna.Framework.Audio.AudioEngine..ctor"):
|
||||
this.Monitor.Log("The game couldn't load audio. Do you have speakers or headphones plugged in?", LogLevel.Error);
|
||||
this.Monitor.Log($"Technical details: {ex.GetLogSummary()}");
|
||||
break;
|
||||
|
||||
// missing content folder exception
|
||||
case FileNotFoundException ex when ex.Message == "Could not find file 'C:\\Program Files (x86)\\Steam\\SteamApps\\common\\Stardew Valley\\Content\\XACT\\FarmerSounds.xgs'.": // path in error is hardcoded regardless of install path
|
||||
this.Monitor.Log("The game can't find its Content\\XACT\\FarmerSounds.xgs file. You can usually fix this by resetting your content files (see https://smapi.io/troubleshoot#reset-content ), or by uninstalling and reinstalling the game.", LogLevel.Error);
|
||||
this.Monitor.Log($"Technical details: {ex.GetLogSummary()}");
|
||||
break;
|
||||
|
||||
// generic exception
|
||||
default:
|
||||
this.MonitorForGame.Log($"The game failed to launch: {exception.GetLogSummary()}", LogLevel.Error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/****
|
||||
** General log output
|
||||
****/
|
||||
/// <summary>Log the initial header with general SMAPI and system details.</summary>
|
||||
/// <param name="modsPath">The path from which mods will be loaded.</param>
|
||||
/// <param name="customSettings">The custom SMAPI settings.</param>
|
||||
public void LogIntro(string modsPath, IDictionary<string, object> customSettings)
|
||||
{
|
||||
// init logging
|
||||
this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GameVersion} on {EnvironmentUtility.GetFriendlyPlatformName(Constants.Platform)}", LogLevel.Info);
|
||||
this.Monitor.Log($"Mods go here: {modsPath}", LogLevel.Info);
|
||||
if (modsPath != Constants.DefaultModsPath)
|
||||
this.Monitor.Log("(Using custom --mods-path argument.)");
|
||||
this.Monitor.Log($"Log started at {DateTime.UtcNow:s} UTC");
|
||||
|
||||
// log custom settings
|
||||
if (customSettings.Any())
|
||||
this.Monitor.Log($"Loaded with custom settings: {string.Join(", ", customSettings.OrderBy(p => p.Key).Select(p => $"{p.Key}: {p.Value}"))}");
|
||||
}
|
||||
|
||||
/// <summary>Log details for settings that don't match the default.</summary>
|
||||
/// <param name="isDeveloperMode">Whether to enable full console output for developers.</param>
|
||||
/// <param name="checkForUpdates">Whether to check for newer versions of SMAPI and mods on startup.</param>
|
||||
public void LogSettingsHeader(bool isDeveloperMode, bool checkForUpdates)
|
||||
{
|
||||
if (isDeveloperMode)
|
||||
this.Monitor.Log($"You have SMAPI for developers, so the console will be much more verbose. You can disable developer mode by installing the non-developer version of SMAPI, or by editing {Constants.ApiConfigPath}.", LogLevel.Info);
|
||||
if (!checkForUpdates)
|
||||
this.Monitor.Log($"You configured SMAPI to not check for updates. Running an old version of SMAPI is not recommended. You can enable update checks by reinstalling SMAPI or editing {Constants.ApiConfigPath}.", LogLevel.Warn);
|
||||
if (!this.Monitor.WriteToConsole)
|
||||
this.Monitor.Log("Writing to the terminal is disabled because the --no-terminal argument was received. This usually means launching the terminal failed.", LogLevel.Warn);
|
||||
this.Monitor.VerboseLog("Verbose logging enabled.");
|
||||
}
|
||||
|
||||
/// <summary>Log info about loaded mods.</summary>
|
||||
/// <param name="loaded">The full list of loaded content packs and mods.</param>
|
||||
/// <param name="loadedContentPacks">The loaded content packs.</param>
|
||||
/// <param name="loadedMods">The loaded mods.</param>
|
||||
/// <param name="skippedMods">The mods which could not be loaded.</param>
|
||||
/// <param name="logParanoidWarnings">Whether to log issues for mods which directly use potentially sensitive .NET APIs like file or shell access.</param>
|
||||
public void LogModInfo(IModMetadata[] loaded, IModMetadata[] loadedContentPacks, IModMetadata[] loadedMods, IModMetadata[] skippedMods, bool logParanoidWarnings)
|
||||
{
|
||||
// log loaded mods
|
||||
this.Monitor.Log($"Loaded {loadedMods.Length} mods" + (loadedMods.Length > 0 ? ":" : "."), LogLevel.Info);
|
||||
foreach (IModMetadata metadata in loadedMods.OrderBy(p => p.DisplayName))
|
||||
{
|
||||
IManifest manifest = metadata.Manifest;
|
||||
this.Monitor.Log(
|
||||
$" {metadata.DisplayName} {manifest.Version}"
|
||||
+ (!string.IsNullOrWhiteSpace(manifest.Author) ? $" by {manifest.Author}" : "")
|
||||
+ (!string.IsNullOrWhiteSpace(manifest.Description) ? $" | {manifest.Description}" : ""),
|
||||
LogLevel.Info
|
||||
);
|
||||
}
|
||||
|
||||
this.Monitor.Newline();
|
||||
|
||||
// log loaded content packs
|
||||
if (loadedContentPacks.Any())
|
||||
{
|
||||
string GetModDisplayName(string id) => loadedMods.FirstOrDefault(p => p.HasID(id))?.DisplayName;
|
||||
|
||||
this.Monitor.Log($"Loaded {loadedContentPacks.Length} content packs:", LogLevel.Info);
|
||||
foreach (IModMetadata metadata in loadedContentPacks.OrderBy(p => p.DisplayName))
|
||||
{
|
||||
IManifest manifest = metadata.Manifest;
|
||||
this.Monitor.Log(
|
||||
$" {metadata.DisplayName} {manifest.Version}"
|
||||
+ (!string.IsNullOrWhiteSpace(manifest.Author) ? $" by {manifest.Author}" : "")
|
||||
+ $" | for {GetModDisplayName(metadata.Manifest.ContentPackFor.UniqueID)}"
|
||||
+ (!string.IsNullOrWhiteSpace(manifest.Description) ? $" | {manifest.Description}" : ""),
|
||||
LogLevel.Info
|
||||
);
|
||||
}
|
||||
|
||||
this.Monitor.Newline();
|
||||
}
|
||||
|
||||
// log mod warnings
|
||||
this.LogModWarnings(loaded, skippedMods, logParanoidWarnings);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
this.LogFile.Dispose();
|
||||
}
|
||||
|
||||
|
||||
/*********
|
||||
** Protected methods
|
||||
*********/
|
||||
/// <summary>Redirect messages logged directly to the console to the given monitor.</summary>
|
||||
/// <param name="gameMonitor">The monitor with which to log messages as the game.</param>
|
||||
/// <param name="message">The message to log.</param>
|
||||
private void HandleConsoleMessage(IMonitor gameMonitor, string message)
|
||||
{
|
||||
// detect exception
|
||||
LogLevel level = message.Contains("Exception") ? LogLevel.Error : LogLevel.Trace;
|
||||
|
||||
// ignore suppressed message
|
||||
if (level != LogLevel.Error && this.SuppressConsolePatterns.Any(p => p.IsMatch(message)))
|
||||
return;
|
||||
|
||||
// show friendly error if applicable
|
||||
foreach (ReplaceLogPattern entry in this.ReplaceConsolePatterns)
|
||||
{
|
||||
string newMessage = entry.Search.Replace(message, entry.Replacement);
|
||||
if (message != newMessage)
|
||||
{
|
||||
gameMonitor.Log(newMessage, entry.LogLevel);
|
||||
gameMonitor.Log(message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// forward to monitor
|
||||
gameMonitor.Log(message, level);
|
||||
}
|
||||
|
||||
/// <summary>Write a summary of mod warnings to the console and log.</summary>
|
||||
/// <param name="mods">The loaded mods.</param>
|
||||
/// <param name="skippedMods">The mods which could not be loaded.</param>
|
||||
/// <param name="logParanoidWarnings">Whether to log issues for mods which directly use potentially sensitive .NET APIs like file or shell access.</param>
|
||||
private void LogModWarnings(IEnumerable<IModMetadata> mods, IModMetadata[] skippedMods, bool logParanoidWarnings)
|
||||
{
|
||||
// get mods with warnings
|
||||
IModMetadata[] modsWithWarnings = mods.Where(p => p.Warnings != ModWarning.None).ToArray();
|
||||
if (!modsWithWarnings.Any() && !skippedMods.Any())
|
||||
return;
|
||||
|
||||
// log intro
|
||||
{
|
||||
int count = modsWithWarnings.Length + skippedMods.Length;
|
||||
this.Monitor.Log($"Found {count} mod{(count == 1 ? "" : "s")} with warnings:", LogLevel.Info);
|
||||
}
|
||||
|
||||
// log skipped mods
|
||||
if (skippedMods.Any())
|
||||
{
|
||||
// get logging logic
|
||||
HashSet<string> loggedDuplicateIds = new HashSet<string>();
|
||||
void LogSkippedMod(IModMetadata mod)
|
||||
{
|
||||
string message = $" - {mod.DisplayName}{(mod.Manifest?.Version != null ? " " + mod.Manifest.Version.ToString() : "")} because {mod.Error}";
|
||||
|
||||
// handle duplicate mods
|
||||
// (log first duplicate only, don't show redundant version)
|
||||
if (mod.FailReason == ModFailReason.Duplicate && mod.HasManifest())
|
||||
{
|
||||
if (!loggedDuplicateIds.Add(mod.Manifest.UniqueID))
|
||||
return; // already logged
|
||||
|
||||
message = $" - {mod.DisplayName} because {mod.Error}";
|
||||
}
|
||||
|
||||
// log message
|
||||
this.Monitor.Log(message, LogLevel.Error);
|
||||
if (mod.ErrorDetails != null)
|
||||
this.Monitor.Log($" ({mod.ErrorDetails})");
|
||||
}
|
||||
|
||||
// find skipped dependencies
|
||||
IModMetadata[] skippedDependencies;
|
||||
{
|
||||
HashSet<string> skippedDependencyIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
HashSet<string> skippedModIds = new HashSet<string>(from mod in skippedMods where mod.HasID() select mod.Manifest.UniqueID, StringComparer.OrdinalIgnoreCase);
|
||||
foreach (IModMetadata mod in skippedMods)
|
||||
{
|
||||
foreach (string requiredId in skippedModIds.Intersect(mod.GetRequiredModIds()))
|
||||
skippedDependencyIds.Add(requiredId);
|
||||
}
|
||||
skippedDependencies = skippedMods.Where(p => p.HasID() && skippedDependencyIds.Contains(p.Manifest.UniqueID)).ToArray();
|
||||
}
|
||||
|
||||
// log skipped mods
|
||||
this.Monitor.Log(" Skipped mods", LogLevel.Error);
|
||||
this.Monitor.Log(" " + "".PadRight(50, '-'), LogLevel.Error);
|
||||
this.Monitor.Log(" These mods could not be added to your game.", LogLevel.Error);
|
||||
this.Monitor.Newline();
|
||||
|
||||
if (skippedDependencies.Any())
|
||||
{
|
||||
foreach (IModMetadata mod in skippedDependencies.OrderBy(p => p.DisplayName))
|
||||
LogSkippedMod(mod);
|
||||
this.Monitor.Newline();
|
||||
}
|
||||
|
||||
foreach (IModMetadata mod in skippedMods.OrderBy(p => p.DisplayName))
|
||||
LogSkippedMod(mod);
|
||||
this.Monitor.Newline();
|
||||
}
|
||||
|
||||
// log warnings
|
||||
if (modsWithWarnings.Any())
|
||||
{
|
||||
// broken code
|
||||
this.LogModWarningGroup(modsWithWarnings, ModWarning.BrokenCodeLoaded, LogLevel.Error, "Broken mods",
|
||||
"These mods have broken code, but you configured SMAPI to load them anyway. This may cause bugs,",
|
||||
"errors, or crashes in-game."
|
||||
);
|
||||
|
||||
// changes serializer
|
||||
this.LogModWarningGroup(modsWithWarnings, ModWarning.ChangesSaveSerializer, LogLevel.Warn, "Changed save serializer",
|
||||
"These mods change the save serializer. They may corrupt your save files, or make them unusable if",
|
||||
"you uninstall these mods."
|
||||
);
|
||||
|
||||
// patched game code
|
||||
this.LogModWarningGroup(modsWithWarnings, ModWarning.PatchesGame, LogLevel.Info, "Patched game code",
|
||||
"These mods directly change the game code. They're more likely to cause errors or bugs in-game; if",
|
||||
"your game has issues, try removing these first. Otherwise you can ignore this warning."
|
||||
);
|
||||
|
||||
// unvalidated update tick
|
||||
this.LogModWarningGroup(modsWithWarnings, ModWarning.UsesUnvalidatedUpdateTick, LogLevel.Info, "Bypassed safety checks",
|
||||
"These mods bypass SMAPI's normal safety checks, so they're more likely to cause errors or save",
|
||||
"corruption. If your game has issues, try removing these first."
|
||||
);
|
||||
|
||||
// paranoid warnings
|
||||
if (logParanoidWarnings)
|
||||
{
|
||||
this.LogModWarningGroup(
|
||||
modsWithWarnings,
|
||||
match: mod => mod.HasUnsuppressedWarnings(ModWarning.AccessesConsole, ModWarning.AccessesFilesystem, ModWarning.AccessesShell),
|
||||
level: LogLevel.Debug,
|
||||
heading: "Direct system access",
|
||||
blurb: new[]
|
||||
{
|
||||
"You enabled paranoid warnings and these mods directly access the filesystem, shells/processes, or",
|
||||
"SMAPI console. (This is usually legitimate and innocent usage; this warning is only useful for",
|
||||
"further investigation.)"
|
||||
},
|
||||
modLabel: mod =>
|
||||
{
|
||||
List<string> labels = new List<string>();
|
||||
if (mod.HasUnsuppressedWarnings(ModWarning.AccessesConsole))
|
||||
labels.Add("console");
|
||||
if (mod.HasUnsuppressedWarnings(ModWarning.AccessesFilesystem))
|
||||
labels.Add("files");
|
||||
if (mod.HasUnsuppressedWarnings(ModWarning.AccessesShell))
|
||||
labels.Add("shells/processes");
|
||||
|
||||
return $"{mod.DisplayName} ({string.Join(", ", labels)})";
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// no update keys
|
||||
this.LogModWarningGroup(modsWithWarnings, ModWarning.NoUpdateKeys, LogLevel.Debug, "No update keys",
|
||||
"These mods have no update keys in their manifest. SMAPI may not notify you about updates for these",
|
||||
"mods. Consider notifying the mod authors about this problem."
|
||||
);
|
||||
|
||||
// not crossplatform
|
||||
this.LogModWarningGroup(modsWithWarnings, ModWarning.UsesDynamic, LogLevel.Debug, "Not crossplatform",
|
||||
"These mods use the 'dynamic' keyword, and won't work on Linux/Mac."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Write a mod warning group to the console and log.</summary>
|
||||
/// <param name="mods">The mods to search.</param>
|
||||
/// <param name="match">Matches mods to include in the warning group.</param>
|
||||
/// <param name="level">The log level for the logged messages.</param>
|
||||
/// <param name="heading">A brief heading label for the group.</param>
|
||||
/// <param name="blurb">A detailed explanation of the warning, split into lines.</param>
|
||||
/// <param name="modLabel">Formats the mod label, or <c>null</c> to use the <see cref="IModMetadata.DisplayName"/>.</param>
|
||||
private void LogModWarningGroup(IModMetadata[] mods, Func<IModMetadata, bool> match, LogLevel level, string heading, string[] blurb, Func<IModMetadata, string> modLabel = null)
|
||||
{
|
||||
// get matching mods
|
||||
string[] modLabels = mods
|
||||
.Where(match)
|
||||
.Select(mod => modLabel?.Invoke(mod) ?? mod.DisplayName)
|
||||
.OrderBy(p => p)
|
||||
.ToArray();
|
||||
if (!modLabels.Any())
|
||||
return;
|
||||
|
||||
// log header/blurb
|
||||
this.Monitor.Log(" " + heading, level);
|
||||
this.Monitor.Log(" " + "".PadRight(50, '-'), level);
|
||||
foreach (string line in blurb)
|
||||
this.Monitor.Log(" " + line, level);
|
||||
this.Monitor.Newline();
|
||||
|
||||
// log mod list
|
||||
foreach (string label in modLabels)
|
||||
this.Monitor.Log($" - {label}", level);
|
||||
|
||||
this.Monitor.Newline();
|
||||
}
|
||||
|
||||
/// <summary>Write a mod warning group to the console and log.</summary>
|
||||
/// <param name="mods">The mods to search.</param>
|
||||
/// <param name="warning">The mod warning to match.</param>
|
||||
/// <param name="level">The log level for the logged messages.</param>
|
||||
/// <param name="heading">A brief heading label for the group.</param>
|
||||
/// <param name="blurb">A detailed explanation of the warning, split into lines.</param>
|
||||
private void LogModWarningGroup(IModMetadata[] mods, ModWarning warning, LogLevel level, string heading, params string[] blurb)
|
||||
{
|
||||
this.LogModWarningGroup(mods, mod => mod.HasUnsuppressedWarnings(warning), level, heading, blurb);
|
||||
}
|
||||
|
||||
|
||||
/*********
|
||||
** Protected types
|
||||
*********/
|
||||
/// <summary>A console log pattern to replace with a different message.</summary>
|
||||
private class ReplaceLogPattern
|
||||
{
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The regex pattern matching the portion of the message to replace.</summary>
|
||||
public Regex Search { get; }
|
||||
|
||||
/// <summary>The replacement string.</summary>
|
||||
public string Replacement { get; }
|
||||
|
||||
/// <summary>The log level for the new message.</summary>
|
||||
public LogLevel LogLevel { get; }
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="search">The regex pattern matching the portion of the message to replace.</param>
|
||||
/// <param name="replacement">The replacement string.</param>
|
||||
/// <param name="logLevel">The log level for the new message.</param>
|
||||
public ReplaceLogPattern(Regex search, string replacement, LogLevel logLevel)
|
||||
{
|
||||
this.Search = search;
|
||||
this.Replacement = replacement;
|
||||
this.LogLevel = logLevel;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace StardewModdingAPI.Framework.ModHelpers
|
||||
namespace StardewModdingAPI.Framework.ModHelpers
|
||||
{
|
||||
/// <summary>The common base class for mod helpers.</summary>
|
||||
internal abstract class BaseHelper : IModLinked
|
||||
@@ -6,7 +6,7 @@
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The unique ID of the mod for which the helper was created.</summary>
|
||||
/// <inheritdoc />
|
||||
public string ModID { get; }
|
||||
|
||||
|
||||
|
||||
@@ -28,23 +28,14 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||
this.CommandManager = commandManager;
|
||||
}
|
||||
|
||||
/// <summary>Add a console command.</summary>
|
||||
/// <param name="name">The command name, which the user must type to trigger it.</param>
|
||||
/// <param name="documentation">The human-readable documentation shown when the player runs the built-in 'help' command.</param>
|
||||
/// <param name="callback">The method to invoke when the command is triggered. This method is passed the command name and arguments submitted by the user.</param>
|
||||
/// <exception cref="ArgumentNullException">The <paramref name="name"/> or <paramref name="callback"/> is null or empty.</exception>
|
||||
/// <exception cref="FormatException">The <paramref name="name"/> is not a valid format.</exception>
|
||||
/// <exception cref="ArgumentException">There's already a command with that name.</exception>
|
||||
/// <inheritdoc />
|
||||
public ICommandHelper Add(string name, string documentation, Action<string, string[]> callback)
|
||||
{
|
||||
this.CommandManager.Add(this.Mod, name, documentation, callback);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>Trigger a command.</summary>
|
||||
/// <param name="name">The command name.</param>
|
||||
/// <param name="arguments">The command arguments.</param>
|
||||
/// <returns>Returns whether a matching command was triggered.</returns>
|
||||
/// <inheritdoc />
|
||||
public bool Trigger(string name, string[] arguments)
|
||||
{
|
||||
return this.CommandManager.Trigger(name, arguments);
|
||||
|
||||
@@ -5,13 +5,10 @@ using System.Diagnostics.CodeAnalysis;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.Xna.Framework.Content;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
using StardewModdingAPI.Framework.Content;
|
||||
using StardewModdingAPI.Framework.ContentManagers;
|
||||
using StardewModdingAPI.Framework.Exceptions;
|
||||
using StardewValley;
|
||||
using xTile;
|
||||
|
||||
namespace StardewModdingAPI.Framework.ModHelpers
|
||||
{
|
||||
@@ -40,10 +37,10 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The game's current locale code (like <c>pt-BR</c>).</summary>
|
||||
/// <inheritdoc />
|
||||
public string CurrentLocale => this.GameContentManager.GetLocale();
|
||||
|
||||
/// <summary>The game's current locale as an enum value.</summary>
|
||||
/// <inheritdoc />
|
||||
public LocalizedContentManager.LanguageCode CurrentLocaleConstant => this.GameContentManager.Language;
|
||||
|
||||
/// <summary>The observable implementation of <see cref="AssetEditors"/>.</summary>
|
||||
@@ -52,10 +49,10 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||
/// <summary>The observable implementation of <see cref="AssetLoaders"/>.</summary>
|
||||
internal ObservableCollection<IAssetLoader> ObservableAssetLoaders { get; } = new ObservableCollection<IAssetLoader>();
|
||||
|
||||
/// <summary>Interceptors which provide the initial versions of matching content assets.</summary>
|
||||
/// <inheritdoc />
|
||||
public IList<IAssetLoader> AssetLoaders => this.ObservableAssetLoaders;
|
||||
|
||||
/// <summary>Interceptors which edit matching content assets after they're loaded.</summary>
|
||||
/// <inheritdoc />
|
||||
public IList<IAssetEditor> AssetEditors => this.ObservableAssetEditors;
|
||||
|
||||
|
||||
@@ -80,12 +77,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||
this.Monitor = monitor;
|
||||
}
|
||||
|
||||
/// <summary>Load content from the game folder or mod folder (if not already cached), and return it. When loading a <c>.png</c> file, this must be called outside the game's draw loop.</summary>
|
||||
/// <typeparam name="T">The expected data type. The main supported types are <see cref="Map"/>, <see cref="Texture2D"/>, and dictionaries; other types may be supported by the game's content pipeline.</typeparam>
|
||||
/// <param name="key">The asset key to fetch (if the <paramref name="source"/> is <see cref="ContentSource.GameContent"/>), or the local path to a content file relative to the mod folder.</param>
|
||||
/// <param name="source">Where to search for a matching content asset.</param>
|
||||
/// <exception cref="ArgumentException">The <paramref name="key"/> is empty or contains invalid characters.</exception>
|
||||
/// <exception cref="ContentLoadException">The content asset couldn't be loaded (e.g. because it doesn't exist).</exception>
|
||||
/// <inheritdoc />
|
||||
public T Load<T>(string key, ContentSource source = ContentSource.ModFolder)
|
||||
{
|
||||
try
|
||||
@@ -109,18 +101,14 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Normalize an asset name so it's consistent with those generated by the game. This is mainly useful for string comparisons like <see cref="string.StartsWith(string)"/> on generated asset names, and isn't necessary when passing asset names into other content helper methods.</summary>
|
||||
/// <param name="assetName">The asset key.</param>
|
||||
/// <inheritdoc />
|
||||
[Pure]
|
||||
public string NormalizeAssetName(string assetName)
|
||||
{
|
||||
return this.ModContentManager.AssertAndNormalizeAssetName(assetName);
|
||||
}
|
||||
|
||||
/// <summary>Get the underlying key in the game's content cache for an asset. This can be used to load custom map tilesheets, but should be avoided when you can use the content API instead. This does not validate whether the asset exists.</summary>
|
||||
/// <param name="key">The asset key to fetch (if the <paramref name="source"/> is <see cref="ContentSource.GameContent"/>), or the local path to a content file relative to the mod folder.</param>
|
||||
/// <param name="source">Where to search for a matching content asset.</param>
|
||||
/// <exception cref="ArgumentException">The <paramref name="key"/> is empty or contains invalid characters.</exception>
|
||||
/// <inheritdoc />
|
||||
public string GetActualAssetKey(string key, ContentSource source = ContentSource.ModFolder)
|
||||
{
|
||||
switch (source)
|
||||
@@ -136,10 +124,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Remove an asset from the content cache so it's reloaded on the next request. This will reload core game assets if needed, but references to the former asset will still show the previous content.</summary>
|
||||
/// <param name="key">The asset key to invalidate in the content folder.</param>
|
||||
/// <exception cref="ArgumentException">The <paramref name="key"/> is empty or contains invalid characters.</exception>
|
||||
/// <returns>Returns whether the given asset key was cached.</returns>
|
||||
/// <inheritdoc />
|
||||
public bool InvalidateCache(string key)
|
||||
{
|
||||
string actualKey = this.GetActualAssetKey(key, ContentSource.GameContent);
|
||||
@@ -147,28 +132,21 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||
return this.ContentCore.InvalidateCache(asset => asset.AssetNameEquals(actualKey)).Any();
|
||||
}
|
||||
|
||||
/// <summary>Remove all assets of the given type from the cache so they're reloaded on the next request. <b>This can be a very expensive operation and should only be used in very specific cases.</b> This will reload core game assets if needed, but references to the former assets will still show the previous content.</summary>
|
||||
/// <typeparam name="T">The asset type to remove from the cache.</typeparam>
|
||||
/// <returns>Returns whether any assets were invalidated.</returns>
|
||||
/// <inheritdoc />
|
||||
public bool InvalidateCache<T>()
|
||||
{
|
||||
this.Monitor.Log($"Requested cache invalidation for all assets of type {typeof(T)}. This is an expensive operation and should be avoided if possible.", LogLevel.Trace);
|
||||
return this.ContentCore.InvalidateCache((key, type) => typeof(T).IsAssignableFrom(type)).Any();
|
||||
}
|
||||
|
||||
/// <summary>Remove matching assets from the content cache so they're reloaded on the next request. This will reload core game assets if needed, but references to the former asset will still show the previous content.</summary>
|
||||
/// <param name="predicate">A predicate matching the assets to invalidate.</param>
|
||||
/// <returns>Returns whether any cache entries were invalidated.</returns>
|
||||
/// <inheritdoc />
|
||||
public bool InvalidateCache(Func<IAssetInfo, bool> predicate)
|
||||
{
|
||||
this.Monitor.Log("Requested cache invalidation for all assets matching a predicate.", LogLevel.Trace);
|
||||
return this.ContentCore.InvalidateCache(predicate).Any();
|
||||
}
|
||||
|
||||
/// <summary>Get a patch helper for arbitrary data.</summary>
|
||||
/// <typeparam name="T">The data type.</typeparam>
|
||||
/// <param name="data">The asset data.</param>
|
||||
/// <param name="assetName">The asset name. This is only used for tracking purposes and has no effect on the patch helper.</param>
|
||||
/// <inheritdoc />
|
||||
public IAssetData GetPatchHelper<T>(T data, string assetName = null)
|
||||
{
|
||||
if (data == null)
|
||||
|
||||
@@ -32,27 +32,20 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||
this.CreateContentPack = createContentPack;
|
||||
}
|
||||
|
||||
/// <summary>Get all content packs loaded for this mod.</summary>
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<IContentPack> GetOwned()
|
||||
{
|
||||
return this.ContentPacks.Value;
|
||||
}
|
||||
|
||||
/// <summary>Create a temporary content pack to read files from a directory, using randomized manifest fields. This will generate fake manifest data; any <c>manifest.json</c> in the directory will be ignored. Temporary content packs will not appear in the SMAPI log and update checks will not be performed.</summary>
|
||||
/// <param name="directoryPath">The absolute directory path containing the content pack files.</param>
|
||||
/// <inheritdoc />
|
||||
public IContentPack CreateFake(string directoryPath)
|
||||
{
|
||||
string id = Guid.NewGuid().ToString("N");
|
||||
return this.CreateTemporary(directoryPath, id, id, id, id, new SemanticVersion(1, 0, 0));
|
||||
}
|
||||
|
||||
/// <summary>Create a temporary content pack to read files from a directory. Temporary content packs will not appear in the SMAPI log and update checks will not be performed.</summary>
|
||||
/// <param name="directoryPath">The absolute directory path containing the content pack files.</param>
|
||||
/// <param name="id">The content pack's unique ID.</param>
|
||||
/// <param name="name">The content pack name.</param>
|
||||
/// <param name="description">The content pack description.</param>
|
||||
/// <param name="author">The content pack author's name.</param>
|
||||
/// <param name="version">The content pack version.</param>
|
||||
/// <inheritdoc />
|
||||
public IContentPack CreateTemporary(string directoryPath, string id, string name, string description, string author, ISemanticVersion version)
|
||||
{
|
||||
// validate
|
||||
|
||||
@@ -39,11 +39,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||
/****
|
||||
** JSON file
|
||||
****/
|
||||
/// <summary>Read data from a JSON file in the mod's folder.</summary>
|
||||
/// <typeparam name="TModel">The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types.</typeparam>
|
||||
/// <param name="path">The file path relative to the mod folder.</param>
|
||||
/// <returns>Returns the deserialized model, or <c>null</c> if the file doesn't exist or is empty.</returns>
|
||||
/// <exception cref="InvalidOperationException">The <paramref name="path"/> is not relative or contains directory climbing (../).</exception>
|
||||
/// <inheritdoc />
|
||||
public TModel ReadJsonFile<TModel>(string path) where TModel : class
|
||||
{
|
||||
if (!PathUtilities.IsSafeRelativePath(path))
|
||||
@@ -55,11 +51,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||
: null;
|
||||
}
|
||||
|
||||
/// <summary>Save data to a JSON file in the mod's folder.</summary>
|
||||
/// <typeparam name="TModel">The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types.</typeparam>
|
||||
/// <param name="path">The file path relative to the mod folder.</param>
|
||||
/// <param name="data">The arbitrary data to save.</param>
|
||||
/// <exception cref="InvalidOperationException">The <paramref name="path"/> is not relative or contains directory climbing (../).</exception>
|
||||
/// <inheritdoc />
|
||||
public void WriteJsonFile<TModel>(string path, TModel data) where TModel : class
|
||||
{
|
||||
if (!PathUtilities.IsSafeRelativePath(path))
|
||||
@@ -72,11 +64,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||
/****
|
||||
** Save file
|
||||
****/
|
||||
/// <summary>Read arbitrary data stored in the current save slot. This is only possible if a save has been loaded.</summary>
|
||||
/// <typeparam name="TModel">The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types.</typeparam>
|
||||
/// <param name="key">The unique key identifying the data.</param>
|
||||
/// <returns>Returns the parsed data, or <c>null</c> if the entry doesn't exist or is empty.</returns>
|
||||
/// <exception cref="InvalidOperationException">The player hasn't loaded a save file yet or isn't the main player.</exception>
|
||||
/// <inheritdoc />
|
||||
public TModel ReadSaveData<TModel>(string key) where TModel : class
|
||||
{
|
||||
if (Context.LoadStage == LoadStage.None)
|
||||
@@ -94,11 +82,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>Save arbitrary data to the current save slot. This is only possible if a save has been loaded, and the data will be lost if the player exits without saving the current day.</summary>
|
||||
/// <typeparam name="TModel">The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types.</typeparam>
|
||||
/// <param name="key">The unique key identifying the data.</param>
|
||||
/// <param name="model">The arbitrary data to save.</param>
|
||||
/// <exception cref="InvalidOperationException">The player hasn't loaded a save file yet or isn't the main player.</exception>
|
||||
/// <inheritdoc />
|
||||
public void WriteSaveData<TModel>(string key, TModel model) where TModel : class
|
||||
{
|
||||
if (Context.LoadStage == LoadStage.None)
|
||||
@@ -123,10 +107,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||
/****
|
||||
** Global app data
|
||||
****/
|
||||
/// <summary>Read arbitrary data stored on the local computer, synchronised by GOG/Steam if applicable.</summary>
|
||||
/// <typeparam name="TModel">The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types.</typeparam>
|
||||
/// <param name="key">The unique key identifying the data.</param>
|
||||
/// <returns>Returns the parsed data, or <c>null</c> if the entry doesn't exist or is empty.</returns>
|
||||
/// <inheritdoc />
|
||||
public TModel ReadGlobalData<TModel>(string key) where TModel : class
|
||||
{
|
||||
string path = this.GetGlobalDataPath(key);
|
||||
@@ -135,10 +116,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||
: null;
|
||||
}
|
||||
|
||||
/// <summary>Save arbitrary data to the local computer, synchronised by GOG/Steam if applicable.</summary>
|
||||
/// <typeparam name="TModel">The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types.</typeparam>
|
||||
/// <param name="key">The unique key identifying the data.</param>
|
||||
/// <param name="data">The arbitrary data to save.</param>
|
||||
/// <inheritdoc />
|
||||
public void WriteGlobalData<TModel>(string key, TModel data) where TModel : class
|
||||
{
|
||||
string path = this.GetGlobalDataPath(key);
|
||||
|
||||
@@ -24,35 +24,31 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||
this.InputState = inputState;
|
||||
}
|
||||
|
||||
/// <summary>Get the current cursor position.</summary>
|
||||
/// <inheritdoc />
|
||||
public ICursorPosition GetCursorPosition()
|
||||
{
|
||||
return this.InputState.CursorPosition;
|
||||
}
|
||||
|
||||
/// <summary>Get whether a button is currently pressed.</summary>
|
||||
/// <param name="button">The button.</param>
|
||||
/// <inheritdoc />
|
||||
public bool IsDown(SButton button)
|
||||
{
|
||||
return this.InputState.IsDown(button);
|
||||
}
|
||||
|
||||
/// <summary>Get whether a button is currently suppressed, so the game won't see it.</summary>
|
||||
/// <param name="button">The button.</param>
|
||||
/// <inheritdoc />
|
||||
public bool IsSuppressed(SButton button)
|
||||
{
|
||||
return this.InputState.IsSuppressed(button);
|
||||
}
|
||||
|
||||
/// <summary>Prevent the game from handling a button press. This doesn't prevent other mods from receiving the event.</summary>
|
||||
/// <param name="button">The button to suppress.</param>
|
||||
/// <inheritdoc />
|
||||
public void Suppress(SButton button)
|
||||
{
|
||||
this.InputState.OverrideButton(button, setDown: false);
|
||||
}
|
||||
|
||||
/// <summary>Get the state of a button.</summary>
|
||||
/// <param name="button">The button to check.</param>
|
||||
/// <inheritdoc />
|
||||
public SButtonState GetState(SButton button)
|
||||
{
|
||||
return this.InputState.GetState(button);
|
||||
|
||||
@@ -2,7 +2,6 @@ using System;
|
||||
using System.IO;
|
||||
using StardewModdingAPI.Events;
|
||||
using StardewModdingAPI.Framework.Input;
|
||||
using StardewModdingAPI.Toolkit.Serialization;
|
||||
|
||||
namespace StardewModdingAPI.Framework.ModHelpers
|
||||
{
|
||||
@@ -12,37 +11,37 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The full path to the mod's folder.</summary>
|
||||
/// <inheritdoc />
|
||||
public string DirectoryPath { get; }
|
||||
|
||||
/// <summary>Manages access to events raised by SMAPI, which let your mod react when something happens in the game.</summary>
|
||||
/// <inheritdoc />
|
||||
public IModEvents Events { get; }
|
||||
|
||||
/// <summary>An API for loading content assets.</summary>
|
||||
/// <inheritdoc />
|
||||
public IContentHelper Content { get; }
|
||||
|
||||
/// <summary>An API for managing content packs.</summary>
|
||||
/// <inheritdoc />
|
||||
public IContentPackHelper ContentPacks { get; }
|
||||
|
||||
/// <summary>An API for reading and writing persistent mod data.</summary>
|
||||
/// <inheritdoc />
|
||||
public IDataHelper Data { get; }
|
||||
|
||||
/// <summary>An API for checking and changing input state.</summary>
|
||||
/// <inheritdoc />
|
||||
public IInputHelper Input { get; }
|
||||
|
||||
/// <summary>An API for accessing private game code.</summary>
|
||||
/// <inheritdoc />
|
||||
public IReflectionHelper Reflection { get; }
|
||||
|
||||
/// <summary>an API for fetching metadata about loaded mods.</summary>
|
||||
/// <inheritdoc />
|
||||
public IModRegistry ModRegistry { get; }
|
||||
|
||||
/// <summary>An API for managing console commands.</summary>
|
||||
/// <inheritdoc />
|
||||
public ICommandHelper ConsoleCommands { get; }
|
||||
|
||||
/// <summary>Provides multiplayer utilities.</summary>
|
||||
/// <inheritdoc />
|
||||
public IMultiplayerHelper Multiplayer { get; }
|
||||
|
||||
/// <summary>An API for reading translations stored in the mod's <c>i18n</c> folder, with one file per locale (like <c>en.json</c>) containing a flat key => value structure. Translations are fetched with locale fallback, so missing translations are filled in from broader locales (like <c>pt-BR.json</c> < <c>pt.json</c> < <c>default.json</c>).</summary>
|
||||
/// <inheritdoc />
|
||||
public ITranslationHelper Translation { get; }
|
||||
|
||||
|
||||
@@ -90,8 +89,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||
/****
|
||||
** Mod config file
|
||||
****/
|
||||
/// <summary>Read the mod's configuration file (and create it if needed).</summary>
|
||||
/// <typeparam name="TConfig">The config class type. This should be a plain class that has public properties for the settings you want. These can be complex types.</typeparam>
|
||||
/// <inheritdoc />
|
||||
public TConfig ReadConfig<TConfig>()
|
||||
where TConfig : class, new()
|
||||
{
|
||||
@@ -100,9 +98,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||
return config;
|
||||
}
|
||||
|
||||
/// <summary>Save to the mod's configuration file.</summary>
|
||||
/// <typeparam name="TConfig">The config class type.</typeparam>
|
||||
/// <param name="config">The config settings to save.</param>
|
||||
/// <inheritdoc />
|
||||
public void WriteConfig<TConfig>(TConfig config)
|
||||
where TConfig : class, new()
|
||||
{
|
||||
@@ -112,7 +108,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||
/****
|
||||
** Disposal
|
||||
****/
|
||||
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
// nothing to dispose yet
|
||||
|
||||
@@ -38,28 +38,25 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||
this.Monitor = monitor;
|
||||
}
|
||||
|
||||
/// <summary>Get metadata for all loaded mods.</summary>
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<IModInfo> GetAll()
|
||||
{
|
||||
return this.Registry.GetAll();
|
||||
}
|
||||
|
||||
/// <summary>Get metadata for a loaded mod.</summary>
|
||||
/// <param name="uniqueID">The mod's unique ID.</param>
|
||||
/// <returns>Returns the matching mod's metadata, or <c>null</c> if not found.</returns>
|
||||
/// <inheritdoc />
|
||||
public IModInfo Get(string uniqueID)
|
||||
{
|
||||
return this.Registry.Get(uniqueID);
|
||||
}
|
||||
|
||||
/// <summary>Get whether a mod has been loaded.</summary>
|
||||
/// <param name="uniqueID">The mod's unique ID.</param>
|
||||
/// <inheritdoc />
|
||||
public bool IsLoaded(string uniqueID)
|
||||
{
|
||||
return this.Registry.Get(uniqueID) != null;
|
||||
}
|
||||
|
||||
/// <summary>Get the API provided by a mod, or <c>null</c> if it has none. This signature requires using the <see cref="IModHelper.Reflection"/> API to access the API's properties and methods.</summary>
|
||||
/// <inheritdoc />
|
||||
public object GetApi(string uniqueID)
|
||||
{
|
||||
// validate ready
|
||||
@@ -76,9 +73,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||
return mod?.Api;
|
||||
}
|
||||
|
||||
/// <summary>Get the API provided by a mod, mapped to a given interface which specifies the expected properties and methods. If the mod has no API or it's not compatible with the given interface, get <c>null</c>.</summary>
|
||||
/// <typeparam name="TInterface">The interface which matches the properties and methods you intend to access.</typeparam>
|
||||
/// <param name="uniqueID">The mod's unique ID.</param>
|
||||
/// <inheritdoc />
|
||||
public TInterface GetApi<TInterface>(string uniqueID) where TInterface : class
|
||||
{
|
||||
// get raw API
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using StardewModdingAPI.Framework.Networking;
|
||||
using StardewValley;
|
||||
@@ -27,21 +26,19 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||
this.Multiplayer = multiplayer;
|
||||
}
|
||||
|
||||
/// <summary>Get a new multiplayer ID.</summary>
|
||||
/// <inheritdoc />
|
||||
public long GetNewID()
|
||||
{
|
||||
return this.Multiplayer.getNewID();
|
||||
}
|
||||
|
||||
/// <summary>Get the locations which are being actively synced from the host.</summary>
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<GameLocation> GetActiveLocations()
|
||||
{
|
||||
return this.Multiplayer.activeLocations();
|
||||
}
|
||||
|
||||
/// <summary>Get a connected player.</summary>
|
||||
/// <param name="id">The player's unique ID.</param>
|
||||
/// <returns>Returns the connected player, or <c>null</c> if no such player is connected.</returns>
|
||||
/// <inheritdoc />
|
||||
public IMultiplayerPeer GetConnectedPlayer(long id)
|
||||
{
|
||||
return this.Multiplayer.Peers.TryGetValue(id, out MultiplayerPeer peer)
|
||||
@@ -49,19 +46,13 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||
: null;
|
||||
}
|
||||
|
||||
/// <summary>Get all connected players.</summary>
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<IMultiplayerPeer> GetConnectedPlayers()
|
||||
{
|
||||
return this.Multiplayer.Peers.Values;
|
||||
}
|
||||
|
||||
/// <summary>Send a message to mods installed by connected players.</summary>
|
||||
/// <typeparam name="TMessage">The data type. This can be a class with a default constructor, or a value type.</typeparam>
|
||||
/// <param name="message">The data to send over the network.</param>
|
||||
/// <param name="messageType">A message type which receiving mods can use to decide whether it's the one they want to handle, like <c>SetPlayerLocation</c>. This doesn't need to be globally unique, since mods should check the originating mod ID.</param>
|
||||
/// <param name="modIDs">The mod IDs which should receive the message on the destination computers, or <c>null</c> for all mods. Specifying mod IDs is recommended to improve performance, unless it's a general-purpose broadcast.</param>
|
||||
/// <param name="playerIDs">The <see cref="Farmer.UniqueMultiplayerID" /> values for the players who should receive the message, or <c>null</c> for all players. If you don't need to broadcast to all players, specifying player IDs is recommended to reduce latency.</param>
|
||||
/// <exception cref="ArgumentNullException">The <paramref name="message"/> or <paramref name="messageType" /> is null.</exception>
|
||||
/// <inheritdoc />
|
||||
public void SendMessage<TMessage>(TMessage message, string messageType, string[] modIDs = null, long[] playerIDs = null)
|
||||
{
|
||||
this.Multiplayer.BroadcastModMessage(
|
||||
|
||||
@@ -32,11 +32,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||
this.Reflector = reflector;
|
||||
}
|
||||
|
||||
/// <summary>Get an instance field.</summary>
|
||||
/// <typeparam name="TValue">The field type.</typeparam>
|
||||
/// <param name="obj">The object which has the field.</param>
|
||||
/// <param name="name">The field name.</param>
|
||||
/// <param name="required">Whether to throw an exception if the field is not found.</param>
|
||||
/// <inheritdoc />
|
||||
public IReflectedField<TValue> GetField<TValue>(object obj, string name, bool required = true)
|
||||
{
|
||||
return this.AssertAccessAllowed(
|
||||
@@ -44,11 +40,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>Get a static field.</summary>
|
||||
/// <typeparam name="TValue">The field type.</typeparam>
|
||||
/// <param name="type">The type which has the field.</param>
|
||||
/// <param name="name">The field name.</param>
|
||||
/// <param name="required">Whether to throw an exception if the field is not found.</param>
|
||||
/// <inheritdoc />
|
||||
public IReflectedField<TValue> GetField<TValue>(Type type, string name, bool required = true)
|
||||
{
|
||||
return this.AssertAccessAllowed(
|
||||
@@ -56,11 +48,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>Get an instance property.</summary>
|
||||
/// <typeparam name="TValue">The property type.</typeparam>
|
||||
/// <param name="obj">The object which has the property.</param>
|
||||
/// <param name="name">The property name.</param>
|
||||
/// <param name="required">Whether to throw an exception if the property is not found.</param>
|
||||
/// <inheritdoc />
|
||||
public IReflectedProperty<TValue> GetProperty<TValue>(object obj, string name, bool required = true)
|
||||
{
|
||||
return this.AssertAccessAllowed(
|
||||
@@ -68,11 +56,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>Get a static property.</summary>
|
||||
/// <typeparam name="TValue">The property type.</typeparam>
|
||||
/// <param name="type">The type which has the property.</param>
|
||||
/// <param name="name">The property name.</param>
|
||||
/// <param name="required">Whether to throw an exception if the property is not found.</param>
|
||||
/// <inheritdoc />
|
||||
public IReflectedProperty<TValue> GetProperty<TValue>(Type type, string name, bool required = true)
|
||||
{
|
||||
return this.AssertAccessAllowed(
|
||||
@@ -80,10 +64,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>Get an instance method.</summary>
|
||||
/// <param name="obj">The object which has the method.</param>
|
||||
/// <param name="name">The field name.</param>
|
||||
/// <param name="required">Whether to throw an exception if the field is not found.</param>
|
||||
/// <inheritdoc />
|
||||
public IReflectedMethod GetMethod(object obj, string name, bool required = true)
|
||||
{
|
||||
return this.AssertAccessAllowed(
|
||||
@@ -91,10 +72,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>Get a static method.</summary>
|
||||
/// <param name="type">The type which has the method.</param>
|
||||
/// <param name="name">The field name.</param>
|
||||
/// <param name="required">Whether to throw an exception if the field is not found.</param>
|
||||
/// <inheritdoc />
|
||||
public IReflectedMethod GetMethod(Type type, string name, bool required = true)
|
||||
{
|
||||
return this.AssertAccessAllowed(
|
||||
|
||||
@@ -16,10 +16,10 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The current locale.</summary>
|
||||
/// <inheritdoc />
|
||||
public string Locale => this.Translator.Locale;
|
||||
|
||||
/// <summary>The game's current language code.</summary>
|
||||
/// <inheritdoc />
|
||||
public LocalizedContentManager.LanguageCode LocaleEnum => this.Translator.LocaleEnum;
|
||||
|
||||
|
||||
@@ -37,22 +37,19 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||
this.Translator.SetLocale(locale, languageCode);
|
||||
}
|
||||
|
||||
/// <summary>Get all translations for the current locale.</summary>
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<Translation> GetTranslations()
|
||||
{
|
||||
return this.Translator.GetTranslations();
|
||||
}
|
||||
|
||||
/// <summary>Get a translation for the current locale.</summary>
|
||||
/// <param name="key">The translation key.</param>
|
||||
/// <inheritdoc />
|
||||
public Translation Get(string key)
|
||||
{
|
||||
return this.Translator.Get(key);
|
||||
}
|
||||
|
||||
/// <summary>Get a translation for the current locale.</summary>
|
||||
/// <param name="key">The translation key.</param>
|
||||
/// <param name="tokens">An object containing token key/value pairs. This can be an anonymous object (like <c>new { value = 42, name = "Cranberries" }</c>), a dictionary, or a class instance.</param>
|
||||
/// <inheritdoc />
|
||||
public Translation Get(string key, object tokens)
|
||||
{
|
||||
return this.Translator.Get(key, tokens);
|
||||
|
||||
@@ -76,10 +76,9 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||
/// <param name="mod">The mod for which the assembly is being loaded.</param>
|
||||
/// <param name="assemblyPath">The assembly file path.</param>
|
||||
/// <param name="assumeCompatible">Assume the mod is compatible, even if incompatible code is detected.</param>
|
||||
/// <param name="rewriteInParallel">Whether to enable experimental parallel rewriting.</param>
|
||||
/// <returns>Returns the rewrite metadata for the preprocessed assembly.</returns>
|
||||
/// <exception cref="IncompatibleInstructionException">An incompatible CIL instruction was found while rewriting the assembly.</exception>
|
||||
public Assembly Load(IModMetadata mod, string assemblyPath, bool assumeCompatible, bool rewriteInParallel)
|
||||
public Assembly Load(IModMetadata mod, string assemblyPath, bool assumeCompatible)
|
||||
{
|
||||
// get referenced local assemblies
|
||||
AssemblyParseResult[] assemblies;
|
||||
@@ -109,7 +108,7 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||
continue;
|
||||
|
||||
// rewrite assembly
|
||||
bool changed = this.RewriteAssembly(mod, assembly.Definition, loggedMessages, logPrefix: " ", rewriteInParallel);
|
||||
bool changed = this.RewriteAssembly(mod, assembly.Definition, loggedMessages, logPrefix: " ");
|
||||
|
||||
// detect broken assembly reference
|
||||
foreach (AssemblyNameReference reference in assembly.Definition.MainModule.AssemblyReferences)
|
||||
@@ -263,10 +262,9 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||
/// <param name="assembly">The assembly to rewrite.</param>
|
||||
/// <param name="loggedMessages">The messages that have already been logged for this mod.</param>
|
||||
/// <param name="logPrefix">A string to prefix to log messages.</param>
|
||||
/// <param name="rewriteInParallel">Whether to enable experimental parallel rewriting.</param>
|
||||
/// <returns>Returns whether the assembly was modified.</returns>
|
||||
/// <exception cref="IncompatibleInstructionException">An incompatible CIL instruction was found while rewriting the assembly.</exception>
|
||||
private bool RewriteAssembly(IModMetadata mod, AssemblyDefinition assembly, HashSet<string> loggedMessages, string logPrefix, bool rewriteInParallel)
|
||||
private bool RewriteAssembly(IModMetadata mod, AssemblyDefinition assembly, HashSet<string> loggedMessages, string logPrefix)
|
||||
{
|
||||
ModuleDefinition module = assembly.MainModule;
|
||||
string filename = $"{assembly.Name.Name}.dll";
|
||||
@@ -294,6 +292,19 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||
IEnumerable<TypeReference> typeReferences = module.GetTypeReferences().OrderBy(p => p.FullName);
|
||||
foreach (TypeReference type in typeReferences)
|
||||
this.ChangeTypeScope(type);
|
||||
|
||||
// rewrite types using custom attributes
|
||||
foreach (TypeDefinition type in module.GetTypes())
|
||||
{
|
||||
foreach (var attr in type.CustomAttributes)
|
||||
{
|
||||
foreach (var conField in attr.ConstructorArguments)
|
||||
{
|
||||
if (conField.Value is TypeReference typeRef)
|
||||
this.ChangeTypeScope(typeRef);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// find or rewrite code
|
||||
@@ -307,15 +318,15 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||
rewritten |= handler.Handle(module, type, replaceWith);
|
||||
return rewritten;
|
||||
},
|
||||
rewriteInstruction: (ref Instruction instruction, ILProcessor cil, Action<Instruction> replaceWith) =>
|
||||
rewriteInstruction: (ref Instruction instruction, ILProcessor cil) =>
|
||||
{
|
||||
bool rewritten = false;
|
||||
foreach (IInstructionHandler handler in handlers)
|
||||
rewritten |= handler.Handle(module, cil, instruction, replaceWith);
|
||||
rewritten |= handler.Handle(module, cil, instruction);
|
||||
return rewritten;
|
||||
}
|
||||
);
|
||||
bool anyRewritten = rewriter.RewriteModule(rewriteInParallel);
|
||||
bool anyRewritten = rewriter.RewriteModule();
|
||||
|
||||
// handle rewrite flags
|
||||
foreach (IInstructionHandler handler in handlers)
|
||||
@@ -398,10 +409,10 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||
if (handler.Phrases.Any())
|
||||
{
|
||||
foreach (string message in handler.Phrases)
|
||||
this.Monitor.LogOnce(template.Replace("$phrase", message));
|
||||
this.Monitor.LogOnce(loggedMessages, template.Replace("$phrase", message));
|
||||
}
|
||||
else
|
||||
this.Monitor.LogOnce(template.Replace("$phrase", handler.DefaultPhrase ?? handler.GetType().Name));
|
||||
this.Monitor.LogOnce(loggedMessages, template.Replace("$phrase", handler.DefaultPhrase ?? handler.GetType().Name));
|
||||
}
|
||||
|
||||
/// <summary>Get the correct reference to use for compatibility with the current platform.</summary>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System;
|
||||
using Mono.Cecil;
|
||||
using Mono.Cecil.Cil;
|
||||
using StardewModdingAPI.Framework.ModLoading.Framework;
|
||||
@@ -36,13 +35,8 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders
|
||||
this.Result = result;
|
||||
}
|
||||
|
||||
/// <summary>Rewrite a CIL instruction reference if needed.</summary>
|
||||
/// <param name="module">The assembly module containing the instruction.</param>
|
||||
/// <param name="cil">The CIL processor.</param>
|
||||
/// <param name="instruction">The CIL instruction to handle.</param>
|
||||
/// <param name="replaceWith">Replaces the CIL instruction with a new one.</param>
|
||||
/// <returns>Returns whether the instruction was changed.</returns>
|
||||
public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action<Instruction> replaceWith)
|
||||
/// <inheritdoc />
|
||||
public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction)
|
||||
{
|
||||
if (!this.Flags.Contains(this.Result) && this.IsMatch(instruction))
|
||||
this.MarkFlag(this.Result);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System;
|
||||
using Mono.Cecil;
|
||||
using Mono.Cecil.Cil;
|
||||
using StardewModdingAPI.Framework.ModLoading.Framework;
|
||||
@@ -36,13 +35,8 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders
|
||||
this.Result = result;
|
||||
}
|
||||
|
||||
/// <summary>Rewrite a CIL instruction reference if needed.</summary>
|
||||
/// <param name="module">The assembly module containing the instruction.</param>
|
||||
/// <param name="cil">The CIL processor.</param>
|
||||
/// <param name="instruction">The CIL instruction to handle.</param>
|
||||
/// <param name="replaceWith">Replaces the CIL instruction with a new one.</param>
|
||||
/// <returns>Returns whether the instruction was changed.</returns>
|
||||
public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action<Instruction> replaceWith)
|
||||
/// <inheritdoc />
|
||||
public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction)
|
||||
{
|
||||
if (!this.Flags.Contains(this.Result) && RewriteHelper.IsFieldReferenceTo(instruction, this.FullTypeName, this.FieldName))
|
||||
this.MarkFlag(this.Result);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System;
|
||||
using Mono.Cecil;
|
||||
using Mono.Cecil.Cil;
|
||||
using StardewModdingAPI.Framework.ModLoading.Framework;
|
||||
@@ -36,13 +35,8 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders
|
||||
this.Result = result;
|
||||
}
|
||||
|
||||
/// <summary>Rewrite a CIL instruction reference if needed.</summary>
|
||||
/// <param name="module">The assembly module containing the instruction.</param>
|
||||
/// <param name="cil">The CIL processor.</param>
|
||||
/// <param name="instruction">The CIL instruction to handle.</param>
|
||||
/// <param name="replaceWith">Replaces the CIL instruction with a new one.</param>
|
||||
/// <returns>Returns whether the instruction was changed.</returns>
|
||||
public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action<Instruction> replaceWith)
|
||||
/// <inheritdoc />
|
||||
public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction)
|
||||
{
|
||||
if (!this.Flags.Contains(this.Result) && this.IsMatch(instruction))
|
||||
this.MarkFlag(this.Result);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System;
|
||||
using Mono.Cecil;
|
||||
using Mono.Cecil.Cil;
|
||||
using StardewModdingAPI.Framework.ModLoading.Framework;
|
||||
@@ -36,13 +35,8 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders
|
||||
this.Result = result;
|
||||
}
|
||||
|
||||
/// <summary>Rewrite a CIL instruction reference if needed.</summary>
|
||||
/// <param name="module">The assembly module containing the instruction.</param>
|
||||
/// <param name="cil">The CIL processor.</param>
|
||||
/// <param name="instruction">The CIL instruction to handle.</param>
|
||||
/// <param name="replaceWith">Replaces the CIL instruction with a new one.</param>
|
||||
/// <returns>Returns whether the instruction was changed.</returns>
|
||||
public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action<Instruction> replaceWith)
|
||||
/// <inheritdoc />
|
||||
public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction)
|
||||
{
|
||||
if (!this.Flags.Contains(this.Result) && this.IsMatch(instruction))
|
||||
this.MarkFlag(this.Result);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Mono.Cecil;
|
||||
@@ -29,13 +28,8 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders
|
||||
this.ValidateReferencesToAssemblies = new HashSet<string>(validateReferencesToAssemblies);
|
||||
}
|
||||
|
||||
/// <summary>Rewrite a CIL instruction reference if needed.</summary>
|
||||
/// <param name="module">The assembly module containing the instruction.</param>
|
||||
/// <param name="cil">The CIL processor.</param>
|
||||
/// <param name="instruction">The CIL instruction to handle.</param>
|
||||
/// <param name="replaceWith">Replaces the CIL instruction with a new one.</param>
|
||||
/// <returns>Returns whether the instruction was changed.</returns>
|
||||
public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action<Instruction> replaceWith)
|
||||
/// <inheritdoc />
|
||||
public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction)
|
||||
{
|
||||
// field reference
|
||||
FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction);
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Mono.Cecil;
|
||||
using Mono.Cecil.Cil;
|
||||
using StardewModdingAPI.Framework.ModLoading.Framework;
|
||||
@@ -29,20 +27,15 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders
|
||||
this.ValidateReferencesToAssemblies = new HashSet<string>(validateReferencesToAssemblies);
|
||||
}
|
||||
|
||||
/// <summary>Rewrite a CIL instruction reference if needed.</summary>
|
||||
/// <param name="module">The assembly module containing the instruction.</param>
|
||||
/// <param name="cil">The CIL processor.</param>
|
||||
/// <param name="instruction">The CIL instruction to handle.</param>
|
||||
/// <param name="replaceWith">Replaces the CIL instruction with a new one.</param>
|
||||
/// <returns>Returns whether the instruction was changed.</returns>
|
||||
public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action<Instruction> replaceWith)
|
||||
/// <inheritdoc />
|
||||
public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction)
|
||||
{
|
||||
// field reference
|
||||
FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction);
|
||||
if (fieldRef != null && this.ShouldValidate(fieldRef.DeclaringType))
|
||||
{
|
||||
FieldDefinition target = fieldRef.DeclaringType.Resolve()?.Fields.FirstOrDefault(p => p.Name == fieldRef.Name);
|
||||
if (target == null)
|
||||
FieldDefinition target = fieldRef.Resolve();
|
||||
if (target == null || target.HasConstant)
|
||||
{
|
||||
this.MarkFlag(InstructionHandleResult.NotCompatible, $"reference to {fieldRef.DeclaringType.FullName}.{fieldRef.Name} (no such field)");
|
||||
return false;
|
||||
@@ -56,7 +49,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders
|
||||
MethodDefinition target = methodRef.Resolve();
|
||||
if (target == null)
|
||||
{
|
||||
string phrase = null;
|
||||
string phrase;
|
||||
if (this.IsProperty(methodRef))
|
||||
phrase = $"reference to {methodRef.DeclaringType.FullName}.{methodRef.Name.Substring(4)} (no such property)";
|
||||
else if (methodRef.Name == ".ctor")
|
||||
|
||||
@@ -35,11 +35,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders
|
||||
this.ShouldIgnore = shouldIgnore;
|
||||
}
|
||||
|
||||
/// <summary>Rewrite a type reference if needed.</summary>
|
||||
/// <param name="module">The assembly module containing the instruction.</param>
|
||||
/// <param name="type">The type definition to handle.</param>
|
||||
/// <param name="replaceWith">Replaces the type reference with a new one.</param>
|
||||
/// <returns>Returns whether the type was changed.</returns>
|
||||
/// <inheritdoc />
|
||||
public override bool Handle(ModuleDefinition module, TypeReference type, Action<TypeReference> replaceWith)
|
||||
{
|
||||
if (type.Scope.Name == this.AssemblyName && this.ShouldIgnore?.Invoke(type) != true)
|
||||
|
||||
@@ -35,11 +35,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders
|
||||
this.ShouldIgnore = shouldIgnore;
|
||||
}
|
||||
|
||||
/// <summary>Rewrite a type reference if needed.</summary>
|
||||
/// <param name="module">The assembly module containing the instruction.</param>
|
||||
/// <param name="type">The type definition to handle.</param>
|
||||
/// <param name="replaceWith">Replaces the type reference with a new one.</param>
|
||||
/// <returns>Returns whether the type was changed.</returns>
|
||||
/// <inheritdoc />
|
||||
public override bool Handle(ModuleDefinition module, TypeReference type, Action<TypeReference> replaceWith)
|
||||
{
|
||||
if (type.FullName == this.FullTypeName && this.ShouldIgnore?.Invoke(type) != true)
|
||||
|
||||
@@ -11,36 +11,27 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>A brief noun phrase indicating what the handler matches, used if <see cref="Phrases"/> is empty.</summary>
|
||||
/// <inheritdoc />
|
||||
public string DefaultPhrase { get; }
|
||||
|
||||
/// <summary>The rewrite flags raised for the current module.</summary>
|
||||
/// <inheritdoc />
|
||||
public ISet<InstructionHandleResult> Flags { get; } = new HashSet<InstructionHandleResult>();
|
||||
|
||||
/// <summary>The brief noun phrases indicating what the handler matched for the current module.</summary>
|
||||
/// <inheritdoc />
|
||||
public ISet<string> Phrases { get; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Rewrite a type reference if needed.</summary>
|
||||
/// <param name="module">The assembly module containing the instruction.</param>
|
||||
/// <param name="type">The type definition to handle.</param>
|
||||
/// <param name="replaceWith">Replaces the type reference with a new one.</param>
|
||||
/// <returns>Returns whether the type was changed.</returns>
|
||||
/// <inheritdoc />
|
||||
public virtual bool Handle(ModuleDefinition module, TypeReference type, Action<TypeReference> replaceWith)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Rewrite a CIL instruction reference if needed.</summary>
|
||||
/// <param name="module">The assembly module containing the instruction.</param>
|
||||
/// <param name="cil">The CIL processor.</param>
|
||||
/// <param name="instruction">The CIL instruction to handle.</param>
|
||||
/// <param name="replaceWith">Replaces the CIL instruction with a new one.</param>
|
||||
/// <returns>Returns whether the instruction was changed.</returns>
|
||||
public virtual bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action<Instruction> replaceWith)
|
||||
/// <inheritdoc />
|
||||
public virtual bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -50,7 +41,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework
|
||||
** Protected methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="defaultPhrase">A brief noun phrase indicating what the handler matches.</param>
|
||||
/// <param name="defaultPhrase">A brief noun phrase indicating what the handler matches, used if <see cref="Phrases"/> is empty.</param>
|
||||
protected BaseInstructionHandler(string defaultPhrase)
|
||||
{
|
||||
this.DefaultPhrase = defaultPhrase;
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Mono.Cecil;
|
||||
using Mono.Cecil.Cil;
|
||||
using Mono.Collections.Generic;
|
||||
@@ -24,9 +22,8 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework
|
||||
/// <summary>Rewrite a CIL instruction in the assembly code.</summary>
|
||||
/// <param name="instruction">The current CIL instruction.</param>
|
||||
/// <param name="cil">The CIL instruction processor.</param>
|
||||
/// <param name="replaceWith">Replaces the CIL instruction with the given instruction.</param>
|
||||
/// <returns>Returns whether the instruction was changed.</returns>
|
||||
public delegate bool RewriteInstructionDelegate(ref Instruction instruction, ILProcessor cil, Action<Instruction> replaceWith);
|
||||
public delegate bool RewriteInstructionDelegate(ref Instruction instruction, ILProcessor cil);
|
||||
|
||||
|
||||
/*********
|
||||
@@ -57,59 +54,24 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework
|
||||
}
|
||||
|
||||
/// <summary>Rewrite the loaded module code.</summary>
|
||||
/// <param name="rewriteInParallel">Whether to enable experimental parallel rewriting.</param>
|
||||
/// <returns>Returns whether the module was modified.</returns>
|
||||
public bool RewriteModule(bool rewriteInParallel)
|
||||
public bool RewriteModule()
|
||||
{
|
||||
IEnumerable<TypeDefinition> types = this.Module.GetTypes().Where(type => type.BaseType != null); // skip special types like <Module>
|
||||
|
||||
// experimental parallel rewriting
|
||||
// This may cause intermittent startup errors and is disabled by default: https://github.com/Pathoschild/SMAPI/issues/721
|
||||
if (rewriteInParallel)
|
||||
bool changed = false;
|
||||
|
||||
try
|
||||
{
|
||||
int typesChanged = 0;
|
||||
Exception exception = null;
|
||||
|
||||
Parallel.ForEach(types, type =>
|
||||
{
|
||||
if (exception != null)
|
||||
return;
|
||||
|
||||
bool changed = false;
|
||||
try
|
||||
{
|
||||
changed = this.RewriteTypeDefinition(type);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exception ??= ex;
|
||||
}
|
||||
|
||||
if (changed)
|
||||
Interlocked.Increment(ref typesChanged);
|
||||
});
|
||||
|
||||
return exception == null
|
||||
? typesChanged > 0
|
||||
: throw new Exception($"Rewriting {this.Module.Name} failed.", exception);
|
||||
foreach (var type in types)
|
||||
changed |= this.RewriteTypeDefinition(type);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Rewriting {this.Module.Name} failed.", ex);
|
||||
}
|
||||
|
||||
// non-parallel rewriting
|
||||
{
|
||||
bool changed = false;
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var type in types)
|
||||
changed |= this.RewriteTypeDefinition(type);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Rewriting {this.Module.Name} failed.", ex);
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
|
||||
@@ -198,12 +160,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework
|
||||
|
||||
// instruction itself
|
||||
// (should be done after the above type rewrites to ensure valid types)
|
||||
rewritten |= this.RewriteInstructionImpl(ref instruction, cil, newInstruction =>
|
||||
{
|
||||
rewritten = true;
|
||||
cil.Replace(instruction, newInstruction);
|
||||
instruction = newInstruction;
|
||||
});
|
||||
rewritten |= this.RewriteInstructionImpl(ref instruction, cil);
|
||||
|
||||
return rewritten;
|
||||
}
|
||||
|
||||
@@ -59,12 +59,30 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework
|
||||
: null;
|
||||
}
|
||||
|
||||
/// <summary>Get the CIL instruction to load a value onto the stack.</summary>
|
||||
/// <param name="rawValue">The constant value to inject.</param>
|
||||
/// <returns>Returns the instruction, or <c>null</c> if the value type isn't supported.</returns>
|
||||
public static Instruction GetLoadValueInstruction(object rawValue)
|
||||
{
|
||||
return rawValue switch
|
||||
{
|
||||
null => Instruction.Create(OpCodes.Ldnull),
|
||||
bool value => Instruction.Create(value ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0),
|
||||
int value => Instruction.Create(OpCodes.Ldc_I4, value), // int32
|
||||
long value => Instruction.Create(OpCodes.Ldc_I8, value), // int64
|
||||
float value => Instruction.Create(OpCodes.Ldc_R4, value), // float32
|
||||
double value => Instruction.Create(OpCodes.Ldc_R8, value), // float64
|
||||
string value => Instruction.Create(OpCodes.Ldstr, value),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Get whether a type matches a type reference.</summary>
|
||||
/// <param name="type">The defined type.</param>
|
||||
/// <param name="reference">The type reference.</param>
|
||||
public static bool IsSameType(Type type, TypeReference reference)
|
||||
{
|
||||
//
|
||||
//
|
||||
// duplicated by IsSameType(TypeReference, TypeReference) below
|
||||
//
|
||||
|
||||
@@ -139,7 +157,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework
|
||||
/// <param name="reference">The method reference.</param>
|
||||
public static bool HasMatchingSignature(MethodBase definition, MethodReference reference)
|
||||
{
|
||||
//
|
||||
//
|
||||
// duplicated by HasMatchingSignature(MethodDefinition, MethodReference) below
|
||||
//
|
||||
|
||||
@@ -165,7 +183,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework
|
||||
/// <param name="reference">The method reference.</param>
|
||||
public static bool HasMatchingSignature(MethodDefinition definition, MethodReference reference)
|
||||
{
|
||||
//
|
||||
//
|
||||
// duplicated by HasMatchingSignature(MethodBase, MethodReference) above
|
||||
//
|
||||
|
||||
|
||||
@@ -35,8 +35,7 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||
/// <param name="module">The assembly module containing the instruction.</param>
|
||||
/// <param name="cil">The CIL processor.</param>
|
||||
/// <param name="instruction">The CIL instruction to handle.</param>
|
||||
/// <param name="replaceWith">Replaces the CIL instruction with a new one.</param>
|
||||
/// <returns>Returns whether the instruction was changed.</returns>
|
||||
bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action<Instruction> replaceWith);
|
||||
bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace StardewModdingAPI.Framework.ModLoading
|
||||
{
|
||||
/// <summary>Indicates why a mod could not be loaded.</summary>
|
||||
internal enum ModFailReason
|
||||
{
|
||||
/// <summary>The mod has been disabled by prefixing its folder with a dot.</summary>
|
||||
DisabledByDotConvention,
|
||||
|
||||
/// <summary>Multiple copies of the mod are installed.</summary>
|
||||
Duplicate,
|
||||
|
||||
/// <summary>The mod has incompatible code instructions, needs a newer SMAPI version, or is marked 'assume broken' in the SMAPI metadata list.</summary>
|
||||
Incompatible,
|
||||
|
||||
/// <summary>The mod's manifest is missing or invalid.</summary>
|
||||
InvalidManifest,
|
||||
|
||||
/// <summary>The mod was deemed compatible, but SMAPI failed when it tried to load it.</summary>
|
||||
LoadFailed,
|
||||
|
||||
/// <summary>The mod requires other mods which aren't installed, or its dependencies have a circular reference.</summary>
|
||||
MissingDependencies,
|
||||
|
||||
/// <summary>The mod is marked obsolete in the SMAPI metadata list.</summary>
|
||||
Obsolete
|
||||
}
|
||||
}
|
||||
@@ -16,55 +16,61 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The mod's display name.</summary>
|
||||
/// <inheritdoc />
|
||||
public string DisplayName { get; }
|
||||
|
||||
/// <summary>The root path containing mods.</summary>
|
||||
/// <inheritdoc />
|
||||
public string RootPath { get; }
|
||||
|
||||
/// <summary>The mod's full directory path within the <see cref="RootPath"/>.</summary>
|
||||
/// <inheritdoc />
|
||||
public string DirectoryPath { get; }
|
||||
|
||||
/// <summary>The <see cref="DirectoryPath"/> relative to the <see cref="RootPath"/>.</summary>
|
||||
/// <inheritdoc />
|
||||
public string RelativeDirectoryPath { get; }
|
||||
|
||||
/// <summary>The mod manifest.</summary>
|
||||
/// <inheritdoc />
|
||||
public IManifest Manifest { get; }
|
||||
|
||||
/// <summary>Metadata about the mod from SMAPI's internal data (if any).</summary>
|
||||
/// <inheritdoc />
|
||||
public ModDataRecordVersionedFields DataRecord { get; }
|
||||
|
||||
/// <summary>The metadata resolution status.</summary>
|
||||
/// <inheritdoc />
|
||||
public ModMetadataStatus Status { get; private set; }
|
||||
|
||||
/// <summary>Indicates non-error issues with the mod.</summary>
|
||||
/// <inheritdoc />
|
||||
public ModFailReason? FailReason { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public ModWarning Warnings { get; private set; }
|
||||
|
||||
/// <summary>The reason the metadata is invalid, if any.</summary>
|
||||
/// <inheritdoc />
|
||||
public string Error { get; private set; }
|
||||
|
||||
/// <summary>Whether the mod folder should be ignored. This is <c>true</c> if it was found within a folder whose name starts with a dot.</summary>
|
||||
/// <inheritdoc />
|
||||
public string ErrorDetails { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsIgnored { get; }
|
||||
|
||||
/// <summary>The mod instance (if loaded and <see cref="IsContentPack"/> is false).</summary>
|
||||
/// <inheritdoc />
|
||||
public IMod Mod { get; private set; }
|
||||
|
||||
/// <summary>The content pack instance (if loaded and <see cref="IsContentPack"/> is true).</summary>
|
||||
/// <inheritdoc />
|
||||
public IContentPack ContentPack { get; private set; }
|
||||
|
||||
/// <summary>The translations for this mod (if loaded).</summary>
|
||||
/// <inheritdoc />
|
||||
public TranslationHelper Translations { get; private set; }
|
||||
|
||||
/// <summary>Writes messages to the console and log file as this mod.</summary>
|
||||
/// <inheritdoc />
|
||||
public IMonitor Monitor { get; private set; }
|
||||
|
||||
/// <summary>The mod-provided API (if any).</summary>
|
||||
/// <inheritdoc />
|
||||
public object Api { get; private set; }
|
||||
|
||||
/// <summary>The update-check metadata for this mod (if any).</summary>
|
||||
/// <inheritdoc />
|
||||
public ModEntryModel UpdateCheckData { get; private set; }
|
||||
|
||||
/// <summary>Whether the mod is a content pack.</summary>
|
||||
/// <inheritdoc />
|
||||
public bool IsContentPack => this.Manifest?.ContentPackFor != null;
|
||||
|
||||
|
||||
@@ -89,28 +95,32 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||
this.IsIgnored = isIgnored;
|
||||
}
|
||||
|
||||
/// <summary>Set the mod status.</summary>
|
||||
/// <param name="status">The metadata resolution status.</param>
|
||||
/// <param name="error">The reason the metadata is invalid, if any.</param>
|
||||
/// <returns>Return the instance for chaining.</returns>
|
||||
public IModMetadata SetStatus(ModMetadataStatus status, string error = null)
|
||||
/// <inheritdoc />
|
||||
public IModMetadata SetStatusFound()
|
||||
{
|
||||
this.Status = status;
|
||||
this.Error = error;
|
||||
this.SetStatus(ModMetadataStatus.Found, ModFailReason.Incompatible, null);
|
||||
this.FailReason = null;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>Set a warning flag for the mod.</summary>
|
||||
/// <param name="warning">The warning to set.</param>
|
||||
/// <inheritdoc />
|
||||
public IModMetadata SetStatus(ModMetadataStatus status, ModFailReason reason, string error, string errorDetails = null)
|
||||
{
|
||||
this.Status = status;
|
||||
this.FailReason = reason;
|
||||
this.Error = error;
|
||||
this.ErrorDetails = errorDetails;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IModMetadata SetWarning(ModWarning warning)
|
||||
{
|
||||
this.Warnings |= warning;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>Set the mod instance.</summary>
|
||||
/// <param name="mod">The mod instance to set.</param>
|
||||
/// <param name="translations">The translations for this mod (if loaded).</param>
|
||||
/// <inheritdoc />
|
||||
public IModMetadata SetMod(IMod mod, TranslationHelper translations)
|
||||
{
|
||||
if (this.ContentPack != null)
|
||||
@@ -122,10 +132,7 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>Set the mod instance.</summary>
|
||||
/// <param name="contentPack">The contentPack instance to set.</param>
|
||||
/// <param name="monitor">Writes messages to the console and log file.</param>
|
||||
/// <param name="translations">The translations for this mod (if loaded).</param>
|
||||
/// <inheritdoc />
|
||||
public IModMetadata SetMod(IContentPack contentPack, IMonitor monitor, TranslationHelper translations)
|
||||
{
|
||||
if (this.Mod != null)
|
||||
@@ -137,29 +144,27 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>Set the mod-provided API instance.</summary>
|
||||
/// <param name="api">The mod-provided API.</param>
|
||||
/// <inheritdoc />
|
||||
public IModMetadata SetApi(object api)
|
||||
{
|
||||
this.Api = api;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>Set the update-check metadata for this mod.</summary>
|
||||
/// <param name="data">The update-check metadata.</param>
|
||||
/// <inheritdoc />
|
||||
public IModMetadata SetUpdateData(ModEntryModel data)
|
||||
{
|
||||
this.UpdateCheckData = data;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>Whether the mod manifest was loaded (regardless of whether the mod itself was loaded).</summary>
|
||||
/// <inheritdoc />
|
||||
public bool HasManifest()
|
||||
{
|
||||
return this.Manifest != null;
|
||||
}
|
||||
|
||||
/// <summary>Whether the mod has an ID (regardless of whether the ID is valid or the mod itself was loaded).</summary>
|
||||
/// <inheritdoc />
|
||||
public bool HasID()
|
||||
{
|
||||
return
|
||||
@@ -167,8 +172,7 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||
&& !string.IsNullOrWhiteSpace(this.Manifest.UniqueID);
|
||||
}
|
||||
|
||||
/// <summary>Whether the mod has the given ID.</summary>
|
||||
/// <param name="id">The mod ID to check.</param>
|
||||
/// <inheritdoc />
|
||||
public bool HasID(string id)
|
||||
{
|
||||
return
|
||||
@@ -176,8 +180,7 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||
&& string.Equals(this.Manifest.UniqueID.Trim(), id?.Trim(), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>Get the defined update keys.</summary>
|
||||
/// <param name="validOnly">Only return valid update keys.</param>
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<UpdateKey> GetUpdateKeys(bool validOnly = false)
|
||||
{
|
||||
foreach (string rawKey in this.Manifest?.UpdateKeys ?? new string[0])
|
||||
@@ -188,8 +191,7 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Get the mod IDs that must be installed to load this mod.</summary>
|
||||
/// <param name="includeOptional">Whether to include optional dependencies.</param>
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<string> GetRequiredModIds(bool includeOptional = false)
|
||||
{
|
||||
HashSet<string> required = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
@@ -209,14 +211,13 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||
yield return this.Manifest.ContentPackFor.UniqueID;
|
||||
}
|
||||
|
||||
/// <summary>Whether the mod has at least one valid update key set.</summary>
|
||||
/// <inheritdoc />
|
||||
public bool HasValidUpdateKeys()
|
||||
{
|
||||
return this.GetUpdateKeys(validOnly: true).Any();
|
||||
}
|
||||
|
||||
/// <summary>Get whether the mod has any of the given warnings which haven't been suppressed in the <see cref="IModMetadata.DataRecord"/>.</summary>
|
||||
/// <param name="warnings">The warnings to check.</param>
|
||||
/// <inheritdoc />
|
||||
public bool HasUnsuppressedWarnings(params ModWarning[] warnings)
|
||||
{
|
||||
return warnings.Any(warning =>
|
||||
@@ -225,7 +226,7 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>Get a relative path which includes the root folder name.</summary>
|
||||
/// <inheritdoc />
|
||||
public string GetRelativePathWithRoot()
|
||||
{
|
||||
string rootFolderName = Path.GetFileName(this.RootPath) ?? "";
|
||||
|
||||
@@ -43,8 +43,13 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||
? ModMetadataStatus.Found
|
||||
: ModMetadataStatus.Failed;
|
||||
|
||||
yield return new ModMetadata(folder.DisplayName, folder.Directory.FullName, rootPath, manifest, dataRecord, isIgnored: shouldIgnore)
|
||||
.SetStatus(status, shouldIgnore ? "disabled by dot convention" : folder.ManifestParseErrorText);
|
||||
var metadata = new ModMetadata(folder.DisplayName, folder.Directory.FullName, rootPath, manifest, dataRecord, isIgnored: shouldIgnore);
|
||||
if (shouldIgnore)
|
||||
metadata.SetStatus(status, ModFailReason.DisabledByDotConvention, "disabled by dot convention");
|
||||
else
|
||||
metadata.SetStatus(status, ModFailReason.InvalidManifest, folder.ManifestParseErrorText);
|
||||
|
||||
yield return metadata;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +72,7 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||
switch (mod.DataRecord?.Status)
|
||||
{
|
||||
case ModStatus.Obsolete:
|
||||
mod.SetStatus(ModMetadataStatus.Failed, $"it's obsolete: {mod.DataRecord.StatusReasonPhrase}");
|
||||
mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.Obsolete, $"it's obsolete: {mod.DataRecord.StatusReasonPhrase}");
|
||||
continue;
|
||||
|
||||
case ModStatus.AssumeBroken:
|
||||
@@ -97,7 +102,7 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||
error += $"version newer than {mod.DataRecord.StatusUpperVersion}";
|
||||
error += " at " + string.Join(" or ", updateUrls);
|
||||
|
||||
mod.SetStatus(ModMetadataStatus.Failed, error);
|
||||
mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.Incompatible, error);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -105,7 +110,7 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||
// validate SMAPI version
|
||||
if (mod.Manifest.MinimumApiVersion?.IsNewerThan(apiVersion) == true)
|
||||
{
|
||||
mod.SetStatus(ModMetadataStatus.Failed, $"it needs SMAPI {mod.Manifest.MinimumApiVersion} or later. Please update SMAPI to the latest version to use this mod.");
|
||||
mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.Incompatible, $"it needs SMAPI {mod.Manifest.MinimumApiVersion} or later. Please update SMAPI to the latest version to use this mod.");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -117,12 +122,12 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||
// validate field presence
|
||||
if (!hasDll && !isContentPack)
|
||||
{
|
||||
mod.SetStatus(ModMetadataStatus.Failed, $"its manifest has no {nameof(IManifest.EntryDll)} or {nameof(IManifest.ContentPackFor)} field; must specify one.");
|
||||
mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest has no {nameof(IManifest.EntryDll)} or {nameof(IManifest.ContentPackFor)} field; must specify one.");
|
||||
continue;
|
||||
}
|
||||
if (hasDll && isContentPack)
|
||||
{
|
||||
mod.SetStatus(ModMetadataStatus.Failed, $"its manifest sets both {nameof(IManifest.EntryDll)} and {nameof(IManifest.ContentPackFor)}, which are mutually exclusive.");
|
||||
mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest sets both {nameof(IManifest.EntryDll)} and {nameof(IManifest.ContentPackFor)}, which are mutually exclusive.");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -132,14 +137,14 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||
// invalid filename format
|
||||
if (mod.Manifest.EntryDll.Intersect(Path.GetInvalidFileNameChars()).Any())
|
||||
{
|
||||
mod.SetStatus(ModMetadataStatus.Failed, $"its manifest has invalid filename '{mod.Manifest.EntryDll}' for the EntryDLL field.");
|
||||
mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest has invalid filename '{mod.Manifest.EntryDll}' for the EntryDLL field.");
|
||||
continue;
|
||||
}
|
||||
|
||||
// invalid path
|
||||
if (!File.Exists(Path.Combine(mod.DirectoryPath, mod.Manifest.EntryDll)))
|
||||
{
|
||||
mod.SetStatus(ModMetadataStatus.Failed, $"its DLL '{mod.Manifest.EntryDll}' doesn't exist.");
|
||||
mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its DLL '{mod.Manifest.EntryDll}' doesn't exist.");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -147,7 +152,7 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||
string actualFilename = new DirectoryInfo(mod.DirectoryPath).GetFiles(mod.Manifest.EntryDll).FirstOrDefault()?.Name;
|
||||
if (actualFilename != mod.Manifest.EntryDll)
|
||||
{
|
||||
mod.SetStatus(ModMetadataStatus.Failed, $"its {nameof(IManifest.EntryDll)} value '{mod.Manifest.EntryDll}' doesn't match the actual file capitalization '{actualFilename}'. The capitalization must match for crossplatform compatibility.");
|
||||
mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its {nameof(IManifest.EntryDll)} value '{mod.Manifest.EntryDll}' doesn't match the actual file capitalization '{actualFilename}'. The capitalization must match for crossplatform compatibility.");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -158,7 +163,7 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||
// invalid content pack ID
|
||||
if (string.IsNullOrWhiteSpace(mod.Manifest.ContentPackFor.UniqueID))
|
||||
{
|
||||
mod.SetStatus(ModMetadataStatus.Failed, $"its manifest declares {nameof(IManifest.ContentPackFor)} without its required {nameof(IManifestContentPackFor.UniqueID)} field.");
|
||||
mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest declares {nameof(IManifest.ContentPackFor)} without its required {nameof(IManifestContentPackFor.UniqueID)} field.");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -177,14 +182,14 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||
|
||||
if (missingFields.Any())
|
||||
{
|
||||
mod.SetStatus(ModMetadataStatus.Failed, $"its manifest is missing required fields ({string.Join(", ", missingFields)}).");
|
||||
mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest is missing required fields ({string.Join(", ", missingFields)}).");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// validate ID format
|
||||
if (!PathUtilities.IsSlug(mod.Manifest.UniqueID))
|
||||
mod.SetStatus(ModMetadataStatus.Failed, "its manifest specifies an invalid ID (IDs must only contain letters, numbers, underscores, periods, or hyphens).");
|
||||
mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, "its manifest specifies an invalid ID (IDs must only contain letters, numbers, underscores, periods, or hyphens).");
|
||||
}
|
||||
|
||||
// validate IDs are unique
|
||||
@@ -199,13 +204,8 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||
if (mod.Status == ModMetadataStatus.Failed)
|
||||
continue; // don't replace metadata error
|
||||
|
||||
string folderList = string.Join(", ",
|
||||
from entry in @group
|
||||
let relativePath = entry.GetRelativePathWithRoot()
|
||||
orderby relativePath
|
||||
select $"{relativePath} ({entry.Manifest.Version})"
|
||||
);
|
||||
mod.SetStatus(ModMetadataStatus.Failed, $"you have multiple copies of this mod installed. Found in folders: {folderList}.");
|
||||
string folderList = string.Join(", ", group.Select(p => p.GetRelativePathWithRoot()).OrderBy(p => p));
|
||||
mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.Duplicate, $"you have multiple copies of this mod installed. To fix this, delete these folders and reinstall the mod: {folderList}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -298,7 +298,7 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||
if (failedModNames.Any())
|
||||
{
|
||||
sortedMods.Push(mod);
|
||||
mod.SetStatus(ModMetadataStatus.Failed, $"it requires mods which aren't installed ({string.Join(", ", failedModNames)}).");
|
||||
mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.MissingDependencies, $"it requires mods which aren't installed ({string.Join(", ", failedModNames)}).");
|
||||
return states[mod] = ModDependencyStatus.Failed;
|
||||
}
|
||||
}
|
||||
@@ -315,7 +315,7 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||
if (failedLabels.Any())
|
||||
{
|
||||
sortedMods.Push(mod);
|
||||
mod.SetStatus(ModMetadataStatus.Failed, $"it needs newer versions of some mods: {string.Join(", ", failedLabels)}.");
|
||||
mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.MissingDependencies, $"it needs newer versions of some mods: {string.Join(", ", failedLabels)}.");
|
||||
return states[mod] = ModDependencyStatus.Failed;
|
||||
}
|
||||
}
|
||||
@@ -338,7 +338,7 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||
if (states[requiredMod] == ModDependencyStatus.Checking)
|
||||
{
|
||||
sortedMods.Push(mod);
|
||||
mod.SetStatus(ModMetadataStatus.Failed, $"its dependencies have a circular reference: {string.Join(" => ", subchain.Select(p => p.DisplayName))} => {requiredMod.DisplayName}).");
|
||||
mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.MissingDependencies, $"its dependencies have a circular reference: {string.Join(" => ", subchain.Select(p => p.DisplayName))} => {requiredMod.DisplayName}).");
|
||||
return states[mod] = ModDependencyStatus.Failed;
|
||||
}
|
||||
|
||||
@@ -354,7 +354,7 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||
// failed, which means this mod can't be loaded either
|
||||
case ModDependencyStatus.Failed:
|
||||
sortedMods.Push(mod);
|
||||
mod.SetStatus(ModMetadataStatus.Failed, $"it needs the '{requiredMod.DisplayName}' mod, which couldn't be loaded.");
|
||||
mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.MissingDependencies, $"it needs the '{requiredMod.DisplayName}' mod, which couldn't be loaded.");
|
||||
return states[mod] = ModDependencyStatus.Failed;
|
||||
|
||||
// unexpected status
|
||||
|
||||
@@ -25,27 +25,32 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="fromType">The type whose field to rewrite.</param>
|
||||
/// <param name="fromFieldName">The field name to rewrite.</param>
|
||||
/// <param name="toType">The new type which will have the field.</param>
|
||||
/// <param name="toFieldName">The new field name to reference.</param>
|
||||
public FieldReplaceRewriter(Type fromType, string fromFieldName, Type toType, string toFieldName)
|
||||
: base(defaultPhrase: $"{fromType.FullName}.{fromFieldName} field")
|
||||
{
|
||||
this.Type = fromType;
|
||||
this.FromFieldName = fromFieldName;
|
||||
this.ToField = toType.GetField(toFieldName);
|
||||
if (this.ToField == null)
|
||||
throw new InvalidOperationException($"The {toType.FullName} class doesn't have a {toFieldName} field.");
|
||||
}
|
||||
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="type">The type whose field to rewrite.</param>
|
||||
/// <param name="fromFieldName">The field name to rewrite.</param>
|
||||
/// <param name="toFieldName">The new field name to reference.</param>
|
||||
public FieldReplaceRewriter(Type type, string fromFieldName, string toFieldName)
|
||||
: base(defaultPhrase: $"{type.FullName}.{fromFieldName} field")
|
||||
: this(type, fromFieldName, type, toFieldName)
|
||||
{
|
||||
this.Type = type;
|
||||
this.FromFieldName = fromFieldName;
|
||||
this.ToField = type.GetField(toFieldName);
|
||||
if (this.ToField == null)
|
||||
throw new InvalidOperationException($"The {type.FullName} class doesn't have a {toFieldName} field.");
|
||||
}
|
||||
|
||||
/// <summary>Rewrite a CIL instruction reference if needed.</summary>
|
||||
/// <param name="module">The assembly module containing the instruction.</param>
|
||||
/// <param name="cil">The CIL processor.</param>
|
||||
/// <param name="instruction">The CIL instruction to handle.</param>
|
||||
/// <param name="replaceWith">Replaces the CIL instruction with a new one.</param>
|
||||
/// <returns>Returns whether the instruction was changed.</returns>
|
||||
public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action<Instruction> replaceWith)
|
||||
/// <inheritdoc />
|
||||
public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction)
|
||||
{
|
||||
// get field reference
|
||||
FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction);
|
||||
@@ -53,8 +58,8 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters
|
||||
return false;
|
||||
|
||||
// replace with new field
|
||||
FieldReference newRef = module.ImportReference(this.ToField);
|
||||
replaceWith(cil.Create(instruction.OpCode, newRef));
|
||||
instruction.Operand = module.ImportReference(this.ToField);
|
||||
|
||||
return this.MarkRewritten();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
using System;
|
||||
using Mono.Cecil;
|
||||
using Mono.Cecil.Cil;
|
||||
using StardewModdingAPI.Framework.ModLoading.Framework;
|
||||
|
||||
namespace StardewModdingAPI.Framework.ModLoading.Rewriters
|
||||
{
|
||||
/// <summary>Rewrites field references into property references.</summary>
|
||||
internal class FieldToPropertyRewriter : BaseInstructionHandler
|
||||
{
|
||||
/*********
|
||||
** Fields
|
||||
*********/
|
||||
/// <summary>The type containing the field to which references should be rewritten.</summary>
|
||||
private readonly Type Type;
|
||||
|
||||
/// <summary>The field name to which references should be rewritten.</summary>
|
||||
private readonly string FromFieldName;
|
||||
|
||||
/// <summary>The new property name.</summary>
|
||||
private readonly string ToPropertyName;
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="type">The type whose field to which references should be rewritten.</param>
|
||||
/// <param name="fieldName">The field name to rewrite.</param>
|
||||
/// <param name="propertyName">The property name (if different).</param>
|
||||
public FieldToPropertyRewriter(Type type, string fieldName, string propertyName)
|
||||
: base(defaultPhrase: $"{type.FullName}.{fieldName} field")
|
||||
{
|
||||
this.Type = type;
|
||||
this.FromFieldName = fieldName;
|
||||
this.ToPropertyName = propertyName;
|
||||
}
|
||||
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="type">The type whose field to which references should be rewritten.</param>
|
||||
/// <param name="fieldName">The field name to rewrite.</param>
|
||||
public FieldToPropertyRewriter(Type type, string fieldName)
|
||||
: this(type, fieldName, fieldName) { }
|
||||
|
||||
/// <summary>Rewrite a CIL instruction reference if needed.</summary>
|
||||
/// <param name="module">The assembly module containing the instruction.</param>
|
||||
/// <param name="cil">The CIL processor.</param>
|
||||
/// <param name="instruction">The CIL instruction to handle.</param>
|
||||
/// <param name="replaceWith">Replaces the CIL instruction with a new one.</param>
|
||||
/// <returns>Returns whether the instruction was changed.</returns>
|
||||
public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action<Instruction> replaceWith)
|
||||
{
|
||||
// get field ref
|
||||
FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction);
|
||||
if (!RewriteHelper.IsFieldReferenceTo(fieldRef, this.Type.FullName, this.FromFieldName))
|
||||
return false;
|
||||
|
||||
// replace with property
|
||||
string methodPrefix = instruction.OpCode == OpCodes.Ldsfld || instruction.OpCode == OpCodes.Ldfld ? "get" : "set";
|
||||
MethodReference propertyRef = module.ImportReference(this.Type.GetMethod($"{methodPrefix}_{this.ToPropertyName}"));
|
||||
replaceWith(cil.Create(OpCodes.Call, propertyRef));
|
||||
return this.MarkRewritten();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,11 +25,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters
|
||||
public Harmony1AssemblyRewriter()
|
||||
: base(defaultPhrase: "Harmony 1.x") { }
|
||||
|
||||
/// <summary>Rewrite a type reference if needed.</summary>
|
||||
/// <param name="module">The assembly module containing the instruction.</param>
|
||||
/// <param name="type">The type definition to handle.</param>
|
||||
/// <param name="replaceWith">Replaces the type reference with a new one.</param>
|
||||
/// <returns>Returns whether the type was changed.</returns>
|
||||
/// <inheritdoc />
|
||||
public override bool Handle(ModuleDefinition module, TypeReference type, Action<TypeReference> replaceWith)
|
||||
{
|
||||
// rewrite Harmony 1.x type to Harmony 2.0 type
|
||||
@@ -45,12 +41,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Rewrite a CIL instruction reference if needed.</summary>
|
||||
/// <param name="module">The assembly module containing the instruction.</param>
|
||||
/// <param name="cil">The CIL processor.</param>
|
||||
/// <param name="instruction">The CIL instruction to handle.</param>
|
||||
/// <param name="replaceWith">Replaces the CIL instruction with a new one.</param>
|
||||
/// <returns>Returns whether the instruction was changed.</returns>
|
||||
/// <inheritdoc />
|
||||
public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action<Instruction> replaceWith)
|
||||
{
|
||||
// rewrite Harmony 1.x methods to Harmony 2.0
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Mono.Cecil;
|
||||
using Mono.Cecil.Cil;
|
||||
using StardewModdingAPI.Framework.ModLoading.Framework;
|
||||
|
||||
namespace StardewModdingAPI.Framework.ModLoading.Rewriters
|
||||
{
|
||||
/// <summary>Automatically fix references to fields that have been replaced by a property or const field.</summary>
|
||||
internal class HeuristicFieldRewriter : BaseInstructionHandler
|
||||
{
|
||||
/*********
|
||||
** Fields
|
||||
*********/
|
||||
/// <summary>The assembly names to which to rewrite broken references.</summary>
|
||||
private readonly HashSet<string> RewriteReferencesToAssemblies;
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="rewriteReferencesToAssemblies">The assembly names to which to rewrite broken references.</param>
|
||||
public HeuristicFieldRewriter(string[] rewriteReferencesToAssemblies)
|
||||
: base(defaultPhrase: "field changed to property") // ignored since we specify phrases
|
||||
{
|
||||
this.RewriteReferencesToAssemblies = new HashSet<string>(rewriteReferencesToAssemblies);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction)
|
||||
{
|
||||
// get field ref
|
||||
FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction);
|
||||
if (fieldRef == null || !this.ShouldValidate(fieldRef.DeclaringType))
|
||||
return false;
|
||||
|
||||
// skip if not broken
|
||||
FieldDefinition fieldDefinition = fieldRef.Resolve();
|
||||
if (fieldDefinition != null && !fieldDefinition.HasConstant)
|
||||
return false;
|
||||
|
||||
// rewrite if possible
|
||||
TypeDefinition declaringType = fieldRef.DeclaringType.Resolve();
|
||||
bool isRead = instruction.OpCode == OpCodes.Ldsfld || instruction.OpCode == OpCodes.Ldfld;
|
||||
return
|
||||
this.TryRewriteToProperty(module, instruction, fieldRef, declaringType, isRead)
|
||||
|| this.TryRewriteToConstField(instruction, fieldDefinition);
|
||||
}
|
||||
|
||||
|
||||
/*********
|
||||
** Private methods
|
||||
*********/
|
||||
/// <summary>Whether references to the given type should be validated.</summary>
|
||||
/// <param name="type">The type reference.</param>
|
||||
private bool ShouldValidate(TypeReference type)
|
||||
{
|
||||
return type != null && this.RewriteReferencesToAssemblies.Contains(type.Scope.Name);
|
||||
}
|
||||
|
||||
/// <summary>Try rewriting the field into a matching property.</summary>
|
||||
/// <param name="module">The assembly module containing the instruction.</param>
|
||||
/// <param name="instruction">The CIL instruction to rewrite.</param>
|
||||
/// <param name="fieldRef">The field reference.</param>
|
||||
/// <param name="declaringType">The type on which the field was defined.</param>
|
||||
/// <param name="isRead">Whether the field is being read; else it's being written to.</param>
|
||||
private bool TryRewriteToProperty(ModuleDefinition module, Instruction instruction, FieldReference fieldRef, TypeDefinition declaringType, bool isRead)
|
||||
{
|
||||
// get equivalent property
|
||||
PropertyDefinition property = declaringType.Properties.FirstOrDefault(p => p.Name == fieldRef.Name);
|
||||
MethodDefinition method = isRead ? property?.GetMethod : property?.SetMethod;
|
||||
if (method == null)
|
||||
return false;
|
||||
|
||||
// rewrite field to property
|
||||
instruction.OpCode = OpCodes.Call;
|
||||
instruction.Operand = module.ImportReference(method);
|
||||
|
||||
this.Phrases.Add($"{fieldRef.DeclaringType.Name}.{fieldRef.Name} (field => property)");
|
||||
return this.MarkRewritten();
|
||||
}
|
||||
|
||||
/// <summary>Try rewriting the field into a matching const field.</summary>
|
||||
/// <param name="instruction">The CIL instruction to rewrite.</param>
|
||||
/// <param name="field">The field definition.</param>
|
||||
private bool TryRewriteToConstField(Instruction instruction, FieldDefinition field)
|
||||
{
|
||||
// must have been a static field read, and the new field must be const
|
||||
if (instruction.OpCode != OpCodes.Ldsfld || field?.HasConstant != true)
|
||||
return false;
|
||||
|
||||
// get opcode for value type
|
||||
Instruction loadInstruction = RewriteHelper.GetLoadValueInstruction(field.Constant);
|
||||
if (loadInstruction == null)
|
||||
return false;
|
||||
|
||||
// rewrite to constant
|
||||
instruction.OpCode = loadInstruction.OpCode;
|
||||
instruction.Operand = loadInstruction.Operand;
|
||||
|
||||
this.Phrases.Add($"{field.DeclaringType.Name}.{field.Name} (field => const)");
|
||||
return this.MarkRewritten();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Mono.Cecil;
|
||||
using Mono.Cecil.Cil;
|
||||
using StardewModdingAPI.Framework.ModLoading.Framework;
|
||||
|
||||
namespace StardewModdingAPI.Framework.ModLoading.Rewriters
|
||||
{
|
||||
/// <summary>Automatically fix references to methods that had extra optional parameters added.</summary>
|
||||
internal class HeuristicMethodRewriter : BaseInstructionHandler
|
||||
{
|
||||
/*********
|
||||
** Fields
|
||||
*********/
|
||||
/// <summary>The assembly names to which to rewrite broken references.</summary>
|
||||
private readonly HashSet<string> RewriteReferencesToAssemblies;
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="rewriteReferencesToAssemblies">The assembly names to which to rewrite broken references.</param>
|
||||
public HeuristicMethodRewriter(string[] rewriteReferencesToAssemblies)
|
||||
: base(defaultPhrase: "methods with missing parameters") // ignored since we specify phrases
|
||||
{
|
||||
this.RewriteReferencesToAssemblies = new HashSet<string>(rewriteReferencesToAssemblies);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction)
|
||||
{
|
||||
// get method ref
|
||||
MethodReference methodRef = RewriteHelper.AsMethodReference(instruction);
|
||||
if (methodRef == null || !this.ShouldValidate(methodRef.DeclaringType))
|
||||
return false;
|
||||
|
||||
// skip if not broken
|
||||
if (methodRef.Resolve() != null)
|
||||
return false;
|
||||
|
||||
// get type
|
||||
var type = methodRef.DeclaringType.Resolve();
|
||||
if (type == null)
|
||||
return false;
|
||||
|
||||
// get method definition
|
||||
MethodDefinition method = null;
|
||||
foreach (var match in type.Methods.Where(p => p.Name == methodRef.Name))
|
||||
{
|
||||
// reference matches initial parameters of definition
|
||||
if (methodRef.Parameters.Count >= match.Parameters.Count || !this.InitialParametersMatch(methodRef, match))
|
||||
continue;
|
||||
|
||||
// all remaining parameters in definition are optional
|
||||
if (!match.Parameters.Skip(methodRef.Parameters.Count).All(p => p.IsOptional))
|
||||
continue;
|
||||
|
||||
method = match;
|
||||
break;
|
||||
}
|
||||
if (method == null)
|
||||
return false;
|
||||
|
||||
// get instructions to inject parameter values
|
||||
var loadInstructions = method.Parameters.Skip(methodRef.Parameters.Count)
|
||||
.Select(p => RewriteHelper.GetLoadValueInstruction(p.Constant))
|
||||
.ToArray();
|
||||
if (loadInstructions.Any(p => p == null))
|
||||
return false; // SMAPI needs to load the value onto the stack before the method call, but the optional parameter type wasn't recognized
|
||||
|
||||
// rewrite method reference
|
||||
foreach (Instruction loadInstruction in loadInstructions)
|
||||
cil.InsertBefore(instruction, loadInstruction);
|
||||
instruction.Operand = module.ImportReference(method);
|
||||
|
||||
this.Phrases.Add($"{methodRef.DeclaringType.Name}.{methodRef.Name} (added missing optional parameters)");
|
||||
return this.MarkRewritten();
|
||||
}
|
||||
|
||||
|
||||
/*********
|
||||
** Private methods
|
||||
*********/
|
||||
/// <summary>Whether references to the given type should be validated.</summary>
|
||||
/// <param name="type">The type reference.</param>
|
||||
private bool ShouldValidate(TypeReference type)
|
||||
{
|
||||
return type != null && this.RewriteReferencesToAssemblies.Contains(type.Scope.Name);
|
||||
}
|
||||
|
||||
/// <summary>Get whether every parameter in the method reference matches the exact order and type of the parameters in the method definition. This ignores extra parameters in the definition.</summary>
|
||||
/// <param name="methodRef">The method reference whose parameters to check.</param>
|
||||
/// <param name="method">The method definition whose parameters to check against.</param>
|
||||
private bool InitialParametersMatch(MethodReference methodRef, MethodDefinition method)
|
||||
{
|
||||
if (methodRef.Parameters.Count > method.Parameters.Count)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < methodRef.Parameters.Count; i++)
|
||||
{
|
||||
if (!RewriteHelper.IsSameType(methodRef.Parameters[i].ParameterType, method.Parameters[i].ParameterType))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,13 +40,8 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters
|
||||
public MethodParentRewriter(Type fromType, Type toType, string nounPhrase = null)
|
||||
: this(fromType.FullName, toType, nounPhrase) { }
|
||||
|
||||
/// <summary>Rewrite a CIL instruction reference if needed.</summary>
|
||||
/// <param name="module">The assembly module containing the instruction.</param>
|
||||
/// <param name="cil">The CIL processor.</param>
|
||||
/// <param name="instruction">The CIL instruction to handle.</param>
|
||||
/// <param name="replaceWith">Replaces the CIL instruction with a new one.</param>
|
||||
/// <returns>Returns whether the instruction was changed.</returns>
|
||||
public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action<Instruction> replaceWith)
|
||||
/// <inheritdoc />
|
||||
public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction)
|
||||
{
|
||||
// get method ref
|
||||
MethodReference methodRef = RewriteHelper.AsMethodReference(instruction);
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
using System;
|
||||
using Mono.Cecil;
|
||||
using Mono.Cecil.Cil;
|
||||
using StardewModdingAPI.Framework.ModLoading.Framework;
|
||||
|
||||
namespace StardewModdingAPI.Framework.ModLoading.Rewriters
|
||||
{
|
||||
/// <summary>Rewrites static field references into constant values.</summary>
|
||||
/// <typeparam name="TValue">The constant value type.</typeparam>
|
||||
internal class StaticFieldToConstantRewriter<TValue> : BaseInstructionHandler
|
||||
{
|
||||
/*********
|
||||
** Fields
|
||||
*********/
|
||||
/// <summary>The type containing the field to which references should be rewritten.</summary>
|
||||
private readonly Type Type;
|
||||
|
||||
/// <summary>The field name to which references should be rewritten.</summary>
|
||||
private readonly string FromFieldName;
|
||||
|
||||
/// <summary>The constant value to replace with.</summary>
|
||||
private readonly TValue Value;
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="type">The type whose field to which references should be rewritten.</param>
|
||||
/// <param name="fieldName">The field name to rewrite.</param>
|
||||
/// <param name="value">The constant value to replace with.</param>
|
||||
public StaticFieldToConstantRewriter(Type type, string fieldName, TValue value)
|
||||
: base(defaultPhrase: $"{type.FullName}.{fieldName} field")
|
||||
{
|
||||
this.Type = type;
|
||||
this.FromFieldName = fieldName;
|
||||
this.Value = value;
|
||||
}
|
||||
|
||||
/// <summary>Rewrite a CIL instruction reference if needed.</summary>
|
||||
/// <param name="module">The assembly module containing the instruction.</param>
|
||||
/// <param name="cil">The CIL processor.</param>
|
||||
/// <param name="instruction">The CIL instruction to handle.</param>
|
||||
/// <param name="replaceWith">Replaces the CIL instruction with a new one.</param>
|
||||
/// <returns>Returns whether the instruction was changed.</returns>
|
||||
public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action<Instruction> replaceWith)
|
||||
{
|
||||
// get field reference
|
||||
FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction);
|
||||
if (!RewriteHelper.IsFieldReferenceTo(fieldRef, this.Type.FullName, this.FromFieldName))
|
||||
return false;
|
||||
|
||||
// rewrite to constant
|
||||
replaceWith(this.CreateConstantInstruction(cil, this.Value));
|
||||
return this.MarkRewritten();
|
||||
}
|
||||
|
||||
|
||||
/*********
|
||||
** Private methods
|
||||
*********/
|
||||
/// <summary>Create a CIL constant value instruction.</summary>
|
||||
/// <param name="cil">The CIL processor.</param>
|
||||
/// <param name="value">The constant value to set.</param>
|
||||
private Instruction CreateConstantInstruction(ILProcessor cil, object value)
|
||||
{
|
||||
if (typeof(TValue) == typeof(int))
|
||||
return cil.Create(OpCodes.Ldc_I4, (int)value);
|
||||
if (typeof(TValue) == typeof(string))
|
||||
return cil.Create(OpCodes.Ldstr, (string)value);
|
||||
throw new NotSupportedException($"Rewriting to constant values of type {typeof(TValue)} isn't currently supported.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,11 +35,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters
|
||||
this.ShouldIgnore = shouldIgnore;
|
||||
}
|
||||
|
||||
/// <summary>Rewrite a type reference if needed.</summary>
|
||||
/// <param name="module">The assembly module containing the instruction.</param>
|
||||
/// <param name="type">The type definition to handle.</param>
|
||||
/// <param name="replaceWith">Replaces the type reference with a new one.</param>
|
||||
/// <returns>Returns whether the type was changed.</returns>
|
||||
/// <inheritdoc />
|
||||
public override bool Handle(ModuleDefinition module, TypeReference type, Action<TypeReference> replaceWith)
|
||||
{
|
||||
// check type reference
|
||||
|
||||
@@ -16,7 +16,6 @@ namespace StardewModdingAPI.Framework.Models
|
||||
{
|
||||
[nameof(CheckForUpdates)] = true,
|
||||
[nameof(ParanoidWarnings)] = Constants.IsDebugBuild,
|
||||
[nameof(RewriteInParallel)] = Constants.IsDebugBuild,
|
||||
[nameof(UseBetaChannel)] = Constants.ApiVersion.IsPrerelease(),
|
||||
[nameof(GitHubProjectName)] = "Pathoschild/SMAPI",
|
||||
[nameof(WebApiBaseUrl)] = "https://smapi.io/api/",
|
||||
@@ -41,9 +40,6 @@ namespace StardewModdingAPI.Framework.Models
|
||||
/// <summary>Whether to check for newer versions of SMAPI and mods on startup.</summary>
|
||||
public bool CheckForUpdates { get; set; }
|
||||
|
||||
/// <summary>Whether to enable experimental parallel rewriting.</summary>
|
||||
public bool RewriteInParallel { get; set; } = (bool)SConfig.DefaultValues[nameof(SConfig.RewriteInParallel)];
|
||||
|
||||
/// <summary>Whether to add a section to the 'mod issues' list for mods which which directly use potentially sensitive .NET APIs like file or shell access.</summary>
|
||||
public bool ParanoidWarnings { get; set; } = (bool)SConfig.DefaultValues[nameof(SConfig.ParanoidWarnings)];
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@ namespace StardewModdingAPI.Framework
|
||||
/// <summary>Handles writing text to the console.</summary>
|
||||
private readonly IConsoleWriter ConsoleWriter;
|
||||
|
||||
/// <summary>Manages access to the console output.</summary>
|
||||
private readonly ConsoleInterceptionManager ConsoleInterceptor;
|
||||
/// <summary>Prefixing a message with this character indicates that the console interceptor should write the string without intercepting it. (The character itself is not written.)</summary>
|
||||
private readonly char IgnoreChar;
|
||||
|
||||
/// <summary>The log file to which to write messages.</summary>
|
||||
private readonly LogFileManager LogFile;
|
||||
@@ -34,7 +34,7 @@ namespace StardewModdingAPI.Framework
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>Whether verbose logging is enabled. This enables more detailed diagnostic messages than are normally needed.</summary>
|
||||
/// <inheritdoc />
|
||||
public bool IsVerbose { get; }
|
||||
|
||||
/// <summary>Whether to show the full log stamps (with time/level/logger) in the console. If false, shows a simplified stamp with only the logger.</summary>
|
||||
@@ -52,11 +52,11 @@ namespace StardewModdingAPI.Framework
|
||||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="source">The name of the module which logs messages using this instance.</param>
|
||||
/// <param name="consoleInterceptor">Intercepts access to the console output.</param>
|
||||
/// <param name="ignoreChar">A character which indicates the message should not be intercepted if it appears as the first character of a string written to the console. The character itself is not logged in that case.</param>
|
||||
/// <param name="logFile">The log file to which to write messages.</param>
|
||||
/// <param name="colorConfig">The colors to use for text written to the SMAPI console.</param>
|
||||
/// <param name="isVerbose">Whether verbose logging is enabled. This enables more detailed diagnostic messages than are normally needed.</param>
|
||||
public Monitor(string source, ConsoleInterceptionManager consoleInterceptor, LogFileManager logFile, ColorSchemeConfig colorConfig, bool isVerbose)
|
||||
public Monitor(string source, char ignoreChar, LogFileManager logFile, ColorSchemeConfig colorConfig, bool isVerbose)
|
||||
{
|
||||
// validate
|
||||
if (string.IsNullOrWhiteSpace(source))
|
||||
@@ -66,29 +66,24 @@ namespace StardewModdingAPI.Framework
|
||||
this.Source = source;
|
||||
this.LogFile = logFile ?? throw new ArgumentNullException(nameof(logFile), "The log file manager cannot be null.");
|
||||
this.ConsoleWriter = new ColorfulConsoleWriter(Constants.Platform, colorConfig);
|
||||
this.ConsoleInterceptor = consoleInterceptor;
|
||||
this.IgnoreChar = ignoreChar;
|
||||
this.IsVerbose = isVerbose;
|
||||
}
|
||||
|
||||
/// <summary>Log a message for the player or developer.</summary>
|
||||
/// <param name="message">The message to log.</param>
|
||||
/// <param name="level">The log severity level.</param>
|
||||
/// <inheritdoc />
|
||||
public void Log(string message, LogLevel level = LogLevel.Trace)
|
||||
{
|
||||
this.LogImpl(this.Source, message, (ConsoleLogLevel)level);
|
||||
}
|
||||
|
||||
/// <summary>Log a message for the player or developer, but only if it hasn't already been logged since the last game launch.</summary>
|
||||
/// <param name="message">The message to log.</param>
|
||||
/// <param name="level">The log severity level.</param>
|
||||
/// <inheritdoc />
|
||||
public void LogOnce(string message, LogLevel level = LogLevel.Trace)
|
||||
{
|
||||
if (this.LogOnceCache.Add($"{message}|{level}"))
|
||||
this.LogImpl(this.Source, message, (ConsoleLogLevel)level);
|
||||
}
|
||||
|
||||
/// <summary>Log a message that only appears when <see cref="IMonitor.IsVerbose"/> is enabled.</summary>
|
||||
/// <param name="message">The message to log.</param>
|
||||
/// <inheritdoc />
|
||||
public void VerboseLog(string message)
|
||||
{
|
||||
if (this.IsVerbose)
|
||||
@@ -99,7 +94,7 @@ namespace StardewModdingAPI.Framework
|
||||
internal void Newline()
|
||||
{
|
||||
if (this.WriteToConsole)
|
||||
this.ConsoleInterceptor.ExclusiveWriteWithoutInterception(Console.WriteLine);
|
||||
Console.WriteLine();
|
||||
this.LogFile.WriteLine("");
|
||||
}
|
||||
|
||||
@@ -136,12 +131,7 @@ namespace StardewModdingAPI.Framework
|
||||
|
||||
// write to console
|
||||
if (this.WriteToConsole && (this.ShowTraceInConsole || level != ConsoleLogLevel.Trace))
|
||||
{
|
||||
this.ConsoleInterceptor.ExclusiveWriteWithoutInterception(() =>
|
||||
{
|
||||
this.ConsoleWriter.WriteLine(consoleMessage, level);
|
||||
});
|
||||
}
|
||||
this.ConsoleWriter.WriteLine(this.IgnoreChar + consoleMessage, level);
|
||||
|
||||
// write to log file
|
||||
this.LogFile.WriteLine(fullMessage);
|
||||
|
||||
@@ -18,25 +18,25 @@ namespace StardewModdingAPI.Framework.Networking
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The player's unique ID.</summary>
|
||||
/// <inheritdoc />
|
||||
public long PlayerID { get; }
|
||||
|
||||
/// <summary>Whether this is a connection to the host player.</summary>
|
||||
/// <inheritdoc />
|
||||
public bool IsHost { get; }
|
||||
|
||||
/// <summary>Whether the player has SMAPI installed.</summary>
|
||||
/// <inheritdoc />
|
||||
public bool HasSmapi => this.ApiVersion != null;
|
||||
|
||||
/// <summary>The player's OS platform, if <see cref="HasSmapi"/> is true.</summary>
|
||||
/// <inheritdoc />
|
||||
public GamePlatform? Platform { get; }
|
||||
|
||||
/// <summary>The installed version of Stardew Valley, if <see cref="HasSmapi"/> is true.</summary>
|
||||
/// <inheritdoc />
|
||||
public ISemanticVersion GameVersion { get; }
|
||||
|
||||
/// <summary>The installed version of SMAPI, if <see cref="HasSmapi"/> is true.</summary>
|
||||
/// <inheritdoc />
|
||||
public ISemanticVersion ApiVersion { get; }
|
||||
|
||||
/// <summary>The installed mods, if <see cref="HasSmapi"/> is true.</summary>
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<IMultiplayerPeerMod> Mods { get; }
|
||||
|
||||
|
||||
@@ -62,9 +62,7 @@ namespace StardewModdingAPI.Framework.Networking
|
||||
this.SendMessageImpl = sendMessage;
|
||||
}
|
||||
|
||||
/// <summary>Get metadata for a mod installed by the player.</summary>
|
||||
/// <param name="id">The unique mod ID.</param>
|
||||
/// <returns>Returns the mod info, or <c>null</c> if the player doesn't have that mod.</returns>
|
||||
/// <inheritdoc />
|
||||
public IMultiplayerPeerMod GetMod(string id)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(id) || this.Mods == null || !this.Mods.Any())
|
||||
|
||||
@@ -5,13 +5,13 @@ namespace StardewModdingAPI.Framework.Networking
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The mod's display name.</summary>
|
||||
/// <inheritdoc />
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>The unique mod ID.</summary>
|
||||
/// <inheritdoc />
|
||||
public string ID { get; }
|
||||
|
||||
/// <summary>The mod version.</summary>
|
||||
/// <inheritdoc />
|
||||
public ISemanticVersion Version { get; }
|
||||
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace StardewModdingAPI.Framework.Reflection
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The reflection metadata.</summary>
|
||||
/// <inheritdoc />
|
||||
public FieldInfo FieldInfo { get; }
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ namespace StardewModdingAPI.Framework.Reflection
|
||||
this.FieldInfo = field;
|
||||
}
|
||||
|
||||
/// <summary>Get the field value.</summary>
|
||||
/// <inheritdoc />
|
||||
public TValue GetValue()
|
||||
{
|
||||
try
|
||||
@@ -72,8 +72,7 @@ namespace StardewModdingAPI.Framework.Reflection
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Set the field value.</summary>
|
||||
//// <param name="value">The value to set.</param>
|
||||
/// <inheritdoc />
|
||||
public void SetValue(TValue value)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace StardewModdingAPI.Framework.Reflection
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The reflection metadata.</summary>
|
||||
/// <inheritdoc />
|
||||
public MethodInfo MethodInfo { get; }
|
||||
|
||||
|
||||
@@ -54,9 +54,7 @@ namespace StardewModdingAPI.Framework.Reflection
|
||||
this.MethodInfo = method;
|
||||
}
|
||||
|
||||
/// <summary>Invoke the method.</summary>
|
||||
/// <typeparam name="TValue">The return type.</typeparam>
|
||||
/// <param name="arguments">The method arguments to pass in.</param>
|
||||
/// <inheritdoc />
|
||||
public TValue Invoke<TValue>(params object[] arguments)
|
||||
{
|
||||
// invoke method
|
||||
@@ -85,8 +83,7 @@ namespace StardewModdingAPI.Framework.Reflection
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Invoke the method.</summary>
|
||||
/// <param name="arguments">The method arguments to pass in.</param>
|
||||
/// <inheritdoc />
|
||||
public void Invoke(params object[] arguments)
|
||||
{
|
||||
// invoke method
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user