Merge branch 'develop' into stable
This commit is contained in:
commit
a1bc96d365
|
@ -34,3 +34,6 @@ appsettings.Development.json
|
||||||
# Azure generated files
|
# Azure generated files
|
||||||
src/SMAPI.Web/Properties/PublishProfiles/*.pubxml
|
src/SMAPI.Web/Properties/PublishProfiles/*.pubxml
|
||||||
src/SMAPI.Web/Properties/ServiceDependencies/* - Web Deploy/
|
src/SMAPI.Web/Properties/ServiceDependencies/* - Web Deploy/
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
|
|
@ -7,12 +7,11 @@ repo. It imports the other MSBuild files as needed.
|
||||||
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!--set general build properties -->
|
<!--set general build properties -->
|
||||||
<Version>3.15.1</Version>
|
<Version>3.16.0</Version>
|
||||||
<Product>SMAPI</Product>
|
<Product>SMAPI</Product>
|
||||||
<LangVersion>latest</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
<AssemblySearchPaths>$(AssemblySearchPaths);{GAC}</AssemblySearchPaths>
|
<AssemblySearchPaths>$(AssemblySearchPaths);{GAC}</AssemblySearchPaths>
|
||||||
<DefineConstants>$(DefineConstants);SMAPI_DEPRECATED</DefineConstants>
|
<DefineConstants>$(DefineConstants);SMAPI_DEPRECATED</DefineConstants>
|
||||||
<DebugType>pdbonly</DebugType>
|
|
||||||
<DebugSymbols>true</DebugSymbols>
|
<DebugSymbols>true</DebugSymbols>
|
||||||
|
|
||||||
<!--enable nullable annotations, except in .NET Standard 2.0 where they aren't supported-->
|
<!--enable nullable annotations, except in .NET Standard 2.0 where they aren't supported-->
|
||||||
|
|
|
@ -4,9 +4,39 @@
|
||||||
<!--
|
<!--
|
||||||
## 4.0.0
|
## 4.0.0
|
||||||
* The installer no longer supports updating from SMAPI 2.11.3 or earlier (released in 2019).
|
* The installer no longer supports updating from SMAPI 2.11.3 or earlier (released in 2019).
|
||||||
_If needed, you can update to SMAPI 3.15.0 first and then install to the latest version._
|
_If needed, you can update to SMAPI 3.16.0 first and then install the latest version._
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
## 3.16.0
|
||||||
|
Released 22 August 2022 for Stardew Valley 1.5.6 or later. See [release highlights](https://www.patreon.com/posts/70797008).
|
||||||
|
|
||||||
|
* For players:
|
||||||
|
* Added error message if mod files are detected directly under `Mods` (instead of each mod having its own subfolder).
|
||||||
|
* SMAPI now sets a success/error code when the game exits.
|
||||||
|
_This is used by your OS (like Windows) to decide whether to keep the console window open when the game ends._
|
||||||
|
* Fixed SMAPI on Windows applying different DPI awareness settings than the game (thanks to spacechase0!).
|
||||||
|
* Fixed Linux/macOS installer's color scheme question partly unreadable if the terminal background is dark.
|
||||||
|
* Fixed error message when a mod loads an invalid PNG file (thanks to atravita!).
|
||||||
|
* Fixed error message when a mod is duplicated, but one of the copies is also missing the DLL file. This now shows the duplicate-mod message instead of the missing-DLL message.
|
||||||
|
* Fixed macOS launcher using Terminal regardless of the system's default terminal (thanks to ishan!).
|
||||||
|
* Fixed best practices in Linux/macOS launcher scripts (thanks to ishan!).
|
||||||
|
* Improved translations. Thanks to KediDili (updated Turkish)!
|
||||||
|
|
||||||
|
* For mod authors:
|
||||||
|
* While loading your mod, SMAPI now searches for indirect dependencies in your mod's folder (thanks to TehPers)! This mainly enables F# mods.
|
||||||
|
* **Raised deprecation message levels.**
|
||||||
|
_Deprecation warnings are now player-visible in the SMAPI console as faded `DEBUG` messages._
|
||||||
|
* Updated to Pintail 2.2.1 (see [changes](https://github.com/Nanoray-pl/Pintail/blob/master/docs/release-notes.md#221)).
|
||||||
|
* Switched SMAPI's `.pdb` files to the newer 'portable' format. This has no effect on mods.
|
||||||
|
|
||||||
|
* For the web UI:
|
||||||
|
* Added log parser warning about performance of PyTK 1.23.0 or earlier.
|
||||||
|
* Converted images to SVG (thanks to ishan!).
|
||||||
|
* Updated log parser for the new update alert format in SMAPI 3.15.1.
|
||||||
|
* Updated the JSON validator/schema for Content Patcher 1.28.0.
|
||||||
|
* Fixed log parsing for invalid content packs.
|
||||||
|
* Fixed log parsing if a mod logged a null character.
|
||||||
|
|
||||||
## 3.15.1
|
## 3.15.1
|
||||||
Released 06 July 2022 for Stardew Valley 1.5.6 or later.
|
Released 06 July 2022 for Stardew Valley 1.5.6 or later.
|
||||||
|
|
||||||
|
@ -41,7 +71,7 @@ Released 17 June 2022 for Stardew Valley 1.5.6 or later. See [release highlights
|
||||||
* Updated dependencies:
|
* Updated dependencies:
|
||||||
* Harmony 2.2.1 (see changes in [2.2.0](https://github.com/pardeike/Harmony/releases/tag/v2.2.0.0) and [2.2.1](https://github.com/pardeike/Harmony/releases/tag/v2.2.1.0));
|
* Harmony 2.2.1 (see changes in [2.2.0](https://github.com/pardeike/Harmony/releases/tag/v2.2.0.0) and [2.2.1](https://github.com/pardeike/Harmony/releases/tag/v2.2.1.0));
|
||||||
* Newtonsoft.Json 13.0.1 (see [changes](https://github.com/JamesNK/Newtonsoft.Json/releases/tag/13.0.1));
|
* Newtonsoft.Json 13.0.1 (see [changes](https://github.com/JamesNK/Newtonsoft.Json/releases/tag/13.0.1));
|
||||||
* Pintail 2.2.0.
|
* Pintail 2.2.0 (see [changes](https://github.com/Nanoray-pl/Pintail/blob/master/docs/release-notes.md#220)).
|
||||||
* Removed transitional `UsePintail` option added in 3.14.0 (now always enabled).
|
* Removed transitional `UsePintail` option added in 3.14.0 (now always enabled).
|
||||||
* Fixed `onBehalfOf` arguments in the new content API being case-sensitive.
|
* Fixed `onBehalfOf` arguments in the new content API being case-sensitive.
|
||||||
* Fixed map edits which change warps sometimes rebuilding the NPC pathfinding cache unnecessarily, which could cause a noticeable delay for players.
|
* Fixed map edits which change warps sometimes rebuilding the NPC pathfinding cache unnecessarily, which could cause a noticeable delay for players.
|
||||||
|
|
|
@ -412,6 +412,9 @@ The NuGet package is generated automatically in `StardewModdingAPI.ModBuildConfi
|
||||||
when you compile it.
|
when you compile it.
|
||||||
|
|
||||||
## Release notes
|
## Release notes
|
||||||
|
### Upcoming release
|
||||||
|
* Switched to the newer crossplatform `portable` debug symbols (thanks to lanturnalis!).
|
||||||
|
|
||||||
### 4.0.1
|
### 4.0.1
|
||||||
Released 14 April 2022.
|
Released 14 April 2022.
|
||||||
|
|
||||||
|
|
|
@ -33,14 +33,15 @@ argument | purpose
|
||||||
`--uninstall` | Preselects the uninstall action, skipping the prompt asking what the user wants to do.
|
`--uninstall` | Preselects the uninstall action, skipping the prompt asking what the user wants to do.
|
||||||
`--game-path "path"` | Specifies the full path to the folder containing the Stardew Valley executable, skipping automatic detection and any prompt to choose a path. If the path is not valid, the installer displays an error.
|
`--game-path "path"` | Specifies the full path to the folder containing the Stardew Valley executable, skipping automatic detection and any prompt to choose a path. If the path is not valid, the installer displays an error.
|
||||||
|
|
||||||
SMAPI itself recognises five arguments **on Windows only**, but these are intended for internal use
|
SMAPI itself recognises five arguments, but these are meant for internal use or testing, and might
|
||||||
or testing and may change without warning. On Linux/macOS, see _environment variables_ below.
|
change without warning. **On Linux/macOS**, command-line arguments won't work; see _environment
|
||||||
|
variables_ below instead.
|
||||||
|
|
||||||
argument | purpose
|
argument | purpose
|
||||||
-------- | -------
|
-------- | -------
|
||||||
`--developer-mode`<br />`--developer-mode-off` | Enable or disable features intended for mod developers. Currently this only makes `TRACE`-level messages appear in the console.
|
`--developer-mode`<br />`--developer-mode-off` | Enable or disable features intended for mod developers. Currently this only makes `TRACE`-level messages appear in the console.
|
||||||
`--no-terminal` | The SMAPI launcher won't try to open a terminal window, and SMAPI won't log anything to the console. (Messages will still be written to the log file.)
|
`--no-terminal` | SMAPI won't log anything to the console. On Linux/macOS only, this will also prevent the launch script from trying to open a terminal window. (Messages will still be written to the log file.)
|
||||||
`--use-current-shell` | The SMAPI launcher won't try to open a terminal window, but SMAPI will still log to the console. (Messages will still be written to the log file.)
|
`--use-current-shell` | On Linux/macOS only, the launch script won't try to open a terminal window. All console output will be sent to the shell running the launch script.
|
||||||
`--mods-path` | The path to search for mods, if not the standard `Mods` folder. This can be a path relative to the game folder (like `--mods-path "Mods (test)"`) or an absolute path.
|
`--mods-path` | The path to search for mods, if not the standard `Mods` folder. This can be a path relative to the game folder (like `--mods-path "Mods (test)"`) or an absolute path.
|
||||||
|
|
||||||
### Environment variables
|
### Environment variables
|
||||||
|
|
|
@ -206,7 +206,7 @@ namespace StardewModdingApi.Installer
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
|
|
||||||
// handle choice
|
// handle choice
|
||||||
string choice = this.InteractivelyChoose("Type 1 or 2, then press enter.", new[] { "1", "2" });
|
string choice = this.InteractivelyChoose("Type 1 or 2, then press enter.", new[] { "1", "2" }, printLine: Console.WriteLine);
|
||||||
switch (choice)
|
switch (choice)
|
||||||
{
|
{
|
||||||
case "1":
|
case "1":
|
||||||
|
@ -629,22 +629,22 @@ namespace StardewModdingApi.Installer
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Interactively ask the user to choose a value.</summary>
|
/// <summary>Interactively ask the user to choose a value.</summary>
|
||||||
/// <param name="print">A callback which prints a message to the console.</param>
|
/// <param name="printLine">A callback which prints a message to the console.</param>
|
||||||
/// <param name="message">The message to print.</param>
|
/// <param name="message">The message to print.</param>
|
||||||
/// <param name="options">The allowed options (not case sensitive).</param>
|
/// <param name="options">The allowed options (not case sensitive).</param>
|
||||||
/// <param name="indent">The indentation to prefix to output.</param>
|
/// <param name="indent">The indentation to prefix to output.</param>
|
||||||
private string InteractivelyChoose(string message, string[] options, string indent = "", Action<string>? print = null)
|
private string InteractivelyChoose(string message, string[] options, string indent = "", Action<string>? printLine = null)
|
||||||
{
|
{
|
||||||
print ??= this.PrintInfo;
|
printLine ??= this.PrintInfo;
|
||||||
|
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
print(indent + message);
|
printLine(indent + message);
|
||||||
Console.Write(indent);
|
Console.Write(indent);
|
||||||
string? input = Console.ReadLine()?.Trim().ToLowerInvariant();
|
string? input = Console.ReadLine()?.Trim().ToLowerInvariant();
|
||||||
if (input == null || !options.Contains(input))
|
if (input == null || !options.Contains(input))
|
||||||
{
|
{
|
||||||
print($"{indent}That's not a valid option.");
|
printLine($"{indent}That's not a valid option.");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
return input;
|
return input;
|
||||||
|
|
|
@ -54,12 +54,12 @@ if [ "$(uname)" == "Darwin" ]; then
|
||||||
# https://stackoverflow.com/a/29511052/262123
|
# https://stackoverflow.com/a/29511052/262123
|
||||||
if [ "$USE_CURRENT_SHELL" == "false" ]; then
|
if [ "$USE_CURRENT_SHELL" == "false" ]; then
|
||||||
echo "Reopening in the Terminal app..."
|
echo "Reopening in the Terminal app..."
|
||||||
echo '#!/bin/sh' > /tmp/open-smapi-terminal.sh
|
echo '#!/bin/sh' > /tmp/open-smapi-terminal.command
|
||||||
echo "\"$0\" $@ --use-current-shell" >> /tmp/open-smapi-terminal.sh
|
echo "\"$0\" $@ --use-current-shell" >> /tmp/open-smapi-terminal.command
|
||||||
chmod +x /tmp/open-smapi-terminal.sh
|
chmod +x /tmp/open-smapi-terminal.command
|
||||||
cat /tmp/open-smapi-terminal.sh
|
cat /tmp/open-smapi-terminal.command
|
||||||
open -W -a Terminal /tmp/open-smapi-terminal.sh
|
open -W /tmp/open-smapi-terminal.command
|
||||||
rm /tmp/open-smapi-terminal.sh
|
rm /tmp/open-smapi-terminal.command
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
@ -71,8 +71,8 @@ fi
|
||||||
##########
|
##########
|
||||||
# script must be run from the game folder
|
# script must be run from the game folder
|
||||||
if [ ! -f "Stardew Valley.dll" ]; then
|
if [ ! -f "Stardew Valley.dll" ]; then
|
||||||
echo "Oops! SMAPI must be placed in the Stardew Valley game folder.\nSee instructions: https://stardewvalleywiki.com/Modding:Player_Guide";
|
printf "Oops! SMAPI must be placed in the Stardew Valley game folder.\nSee instructions: https://stardewvalleywiki.com/Modding:Player_Guide";
|
||||||
read
|
read -r
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
@ -102,37 +102,39 @@ else
|
||||||
|
|
||||||
# find the true shell behind x-terminal-emulator
|
# find the true shell behind x-terminal-emulator
|
||||||
if [ "$TERMINAL_NAME" = "x-terminal-emulator" ]; then
|
if [ "$TERMINAL_NAME" = "x-terminal-emulator" ]; then
|
||||||
export TERMINAL_NAME="$(basename "$(readlink -f $(command -v x-terminal-emulator))")"
|
TERMINAL_NAME="$(basename "$(readlink -f "$(command -v x-terminal-emulator)")")"
|
||||||
|
export TERMINAL_NAME
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# run in selected terminal and account for quirks
|
# run in selected terminal and account for quirks
|
||||||
export TERMINAL_PATH="$(command -v $TERMINAL_NAME)"
|
TERMINAL_PATH="$(command -v "$TERMINAL_NAME")"
|
||||||
if [ -x $TERMINAL_PATH ]; then
|
export TERMINAL_PATH
|
||||||
|
if [ -x "$TERMINAL_PATH" ]; then
|
||||||
case $TERMINAL_NAME in
|
case $TERMINAL_NAME in
|
||||||
terminal|termite)
|
terminal|termite)
|
||||||
# consumes only one argument after -e
|
# consumes only one argument after -e
|
||||||
# options containing space characters are unsupported
|
# options containing space characters are unsupported
|
||||||
exec $TERMINAL_NAME -e "env TERM=xterm $LAUNCH_FILE $@"
|
exec "$TERMINAL_NAME" -e "env TERM=xterm $LAUNCH_FILE $@"
|
||||||
;;
|
;;
|
||||||
|
|
||||||
xterm|konsole|alacritty)
|
xterm|konsole|alacritty)
|
||||||
# consumes all arguments after -e
|
# consumes all arguments after -e
|
||||||
exec $TERMINAL_NAME -e env TERM=xterm $LAUNCH_FILE "$@"
|
exec "$TERMINAL_NAME" -e env TERM=xterm $LAUNCH_FILE "$@"
|
||||||
;;
|
;;
|
||||||
|
|
||||||
terminator|xfce4-terminal|mate-terminal)
|
terminator|xfce4-terminal|mate-terminal)
|
||||||
# consumes all arguments after -x
|
# consumes all arguments after -x
|
||||||
exec $TERMINAL_NAME -x env TERM=xterm $LAUNCH_FILE "$@"
|
exec "$TERMINAL_NAME" -x env TERM=xterm $LAUNCH_FILE "$@"
|
||||||
;;
|
;;
|
||||||
|
|
||||||
gnome-terminal)
|
gnome-terminal)
|
||||||
# consumes all arguments after --
|
# consumes all arguments after --
|
||||||
exec $TERMINAL_NAME -- env TERM=xterm $LAUNCH_FILE "$@"
|
exec "$TERMINAL_NAME" -- env TERM=xterm $LAUNCH_FILE "$@"
|
||||||
;;
|
;;
|
||||||
|
|
||||||
kitty)
|
kitty)
|
||||||
# consumes all trailing arguments
|
# consumes all trailing arguments
|
||||||
exec $TERMINAL_NAME env TERM=xterm $LAUNCH_FILE "$@"
|
exec "$TERMINAL_NAME" env TERM=xterm $LAUNCH_FILE "$@"
|
||||||
;;
|
;;
|
||||||
|
|
||||||
*)
|
*)
|
||||||
|
|
|
@ -8,8 +8,7 @@
|
||||||
** Set build options
|
** Set build options
|
||||||
**********************************************-->
|
**********************************************-->
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- include PDB file by default to enable line numbers in stack traces -->
|
<!-- enable line numbers in stack traces -->
|
||||||
<DebugType>pdbonly</DebugType>
|
|
||||||
<DebugSymbols>true</DebugSymbols>
|
<DebugSymbols>true</DebugSymbols>
|
||||||
|
|
||||||
<!-- don't create the 'refs' folder (which isn't useful for mods) -->
|
<!-- don't create the 'refs' folder (which isn't useful for mods) -->
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
{
|
{
|
||||||
"Name": "Console Commands",
|
"Name": "Console Commands",
|
||||||
"Author": "SMAPI",
|
"Author": "SMAPI",
|
||||||
"Version": "3.15.1",
|
"Version": "3.16.0",
|
||||||
"Description": "Adds SMAPI console commands that let you manipulate the game.",
|
"Description": "Adds SMAPI console commands that let you manipulate the game.",
|
||||||
"UniqueID": "SMAPI.ConsoleCommands",
|
"UniqueID": "SMAPI.ConsoleCommands",
|
||||||
"EntryDll": "ConsoleCommands.dll",
|
"EntryDll": "ConsoleCommands.dll",
|
||||||
"MinimumApiVersion": "3.15.1"
|
"MinimumApiVersion": "3.16.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
{
|
{
|
||||||
"Name": "Error Handler",
|
"Name": "Error Handler",
|
||||||
"Author": "SMAPI",
|
"Author": "SMAPI",
|
||||||
"Version": "3.15.1",
|
"Version": "3.16.0",
|
||||||
"Description": "Handles some common vanilla errors to log more useful info or avoid breaking the game.",
|
"Description": "Handles some common vanilla errors to log more useful info or avoid breaking the game.",
|
||||||
"UniqueID": "SMAPI.ErrorHandler",
|
"UniqueID": "SMAPI.ErrorHandler",
|
||||||
"EntryDll": "ErrorHandler.dll",
|
"EntryDll": "ErrorHandler.dll",
|
||||||
"MinimumApiVersion": "3.15.1"
|
"MinimumApiVersion": "3.16.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
{
|
{
|
||||||
"Name": "Save Backup",
|
"Name": "Save Backup",
|
||||||
"Author": "SMAPI",
|
"Author": "SMAPI",
|
||||||
"Version": "3.15.1",
|
"Version": "3.16.0",
|
||||||
"Description": "Automatically backs up all your saves once per day into its folder.",
|
"Description": "Automatically backs up all your saves once per day into its folder.",
|
||||||
"UniqueID": "SMAPI.SaveBackup",
|
"UniqueID": "SMAPI.SaveBackup",
|
||||||
"EntryDll": "SaveBackup.dll",
|
"EntryDll": "SaveBackup.dll",
|
||||||
"MinimumApiVersion": "3.15.1"
|
"MinimumApiVersion": "3.16.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Collections.Specialized;
|
||||||
|
using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Web;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using StardewModdingAPI.Toolkit.Utilities;
|
using StardewModdingAPI.Toolkit.Utilities;
|
||||||
using StardewModdingAPI.Web.Framework;
|
using StardewModdingAPI.Web.Framework;
|
||||||
|
@ -87,9 +89,15 @@ namespace StardewModdingAPI.Web.Controllers
|
||||||
public async Task<ActionResult> PostAsync()
|
public async Task<ActionResult> PostAsync()
|
||||||
{
|
{
|
||||||
// get raw log text
|
// get raw log text
|
||||||
string? input = this.Request.Form["input"].FirstOrDefault();
|
// note: avoid this.Request.Form, which fails if any mod logged a null character.
|
||||||
|
string? input;
|
||||||
|
{
|
||||||
|
using StreamReader reader = new StreamReader(this.Request.Body);
|
||||||
|
NameValueCollection parsed = HttpUtility.ParseQueryString(await reader.ReadToEndAsync());
|
||||||
|
input = parsed["input"];
|
||||||
if (string.IsNullOrWhiteSpace(input))
|
if (string.IsNullOrWhiteSpace(input))
|
||||||
return this.View("Index", this.GetModel(null, uploadError: "The log file seems to be empty."));
|
return this.View("Index", this.GetModel(null, uploadError: "The log file seems to be empty."));
|
||||||
|
}
|
||||||
|
|
||||||
// upload log
|
// upload log
|
||||||
UploadResult uploadResult = await this.Storage.SaveAsync(input);
|
UploadResult uploadResult = await this.Storage.SaveAsync(input);
|
||||||
|
|
|
@ -36,13 +36,13 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
|
||||||
private readonly Regex ContentPackListStartPattern = new(@"^Loaded \d+ content packs:$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
private readonly Regex ContentPackListStartPattern = new(@"^Loaded \d+ content packs:$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
/// <summary>A regex pattern matching an entry in SMAPI's content pack list.</summary>
|
/// <summary>A regex pattern matching an entry in SMAPI's content pack list.</summary>
|
||||||
private readonly Regex ContentPackListEntryPattern = new(@"^ (?<name>.+?) (?<version>[^\s]+)(?: by (?<author>[^\|]+))? \| for (?<for>[^\|]+)(?: \| (?<description>.+))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
private readonly Regex ContentPackListEntryPattern = new(@"^ (?<name>.+?) (?<version>[^\s]+)(?: by (?<author>[^\|]+))? \| for (?<for>[^\|]*)(?: \| (?<description>.+))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
/// <summary>A regex pattern matching the start of SMAPI's mod update list.</summary>
|
/// <summary>A regex pattern matching the start of SMAPI's mod update list.</summary>
|
||||||
private readonly Regex ModUpdateListStartPattern = new(@"^You can update \d+ mods?:$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
private readonly Regex ModUpdateListStartPattern = new(@"^You can update \d+ mods?:$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
/// <summary>A regex pattern matching an entry in SMAPI's mod update list.</summary>
|
/// <summary>A regex pattern matching an entry in SMAPI's mod update list.</summary>
|
||||||
private readonly Regex ModUpdateListEntryPattern = new(@"^ (?<name>.+) (?<version>[^\s]+): (?<link>.+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
private readonly Regex ModUpdateListEntryPattern = new(@"^ (?<name>.+) (?<version>[^\s]+): (?<link>[^\s]+)(?: \(you have [^\)]+\))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
/// <summary>A regex pattern matching SMAPI's update line.</summary>
|
/// <summary>A regex pattern matching SMAPI's update line.</summary>
|
||||||
private readonly Regex SmapiUpdatePattern = new(@"^You can update SMAPI to (?<version>[^\s]+): (?<link>.+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
private readonly Regex SmapiUpdatePattern = new(@"^You can update SMAPI to (?<version>[^\s]+): (?<link>.+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
@ -77,8 +77,8 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
|
||||||
};
|
};
|
||||||
|
|
||||||
// parse log messages
|
// parse log messages
|
||||||
LogModInfo smapiMod = new(name: "SMAPI", author: "Pathoschild", version: "", description: "", loaded: true, isMod: false);
|
LogModInfo smapiMod = new(ModType.Special, name: "SMAPI", author: "Pathoschild", version: "", description: "", loaded: true);
|
||||||
LogModInfo gameMod = new(name: "game", author: "", version: "", description: "", loaded: true, isMod: false);
|
LogModInfo gameMod = new(ModType.Special, name: "game", author: "", version: "", description: "", loaded: true);
|
||||||
IDictionary<string, List<LogModInfo>> mods = new Dictionary<string, List<LogModInfo>>();
|
IDictionary<string, List<LogModInfo>> mods = new Dictionary<string, List<LogModInfo>>();
|
||||||
bool inModList = false;
|
bool inModList = false;
|
||||||
bool inContentPackList = false;
|
bool inContentPackList = false;
|
||||||
|
@ -133,7 +133,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
|
||||||
|
|
||||||
if (!mods.TryGetValue(name, out List<LogModInfo>? entries))
|
if (!mods.TryGetValue(name, out List<LogModInfo>? entries))
|
||||||
mods[name] = entries = new List<LogModInfo>();
|
mods[name] = entries = new List<LogModInfo>();
|
||||||
entries.Add(new LogModInfo(name: name, author: author, version: version, description: description, loaded: true));
|
entries.Add(new LogModInfo(ModType.CodeMod, name: name, author: author, version: version, description: description, loaded: true));
|
||||||
|
|
||||||
message.Section = LogSection.ModsList;
|
message.Section = LogSection.ModsList;
|
||||||
}
|
}
|
||||||
|
@ -156,7 +156,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
|
||||||
|
|
||||||
if (!mods.TryGetValue(name, out List<LogModInfo>? entries))
|
if (!mods.TryGetValue(name, out List<LogModInfo>? entries))
|
||||||
mods[name] = entries = new List<LogModInfo>();
|
mods[name] = entries = new List<LogModInfo>();
|
||||||
entries.Add(new LogModInfo(name: name, author: author, version: version, description: description, contentPackFor: forMod, loaded: true));
|
entries.Add(new LogModInfo(ModType.ContentPack, name: name, author: author, version: version, description: description, contentPackFor: forMod, loaded: true));
|
||||||
|
|
||||||
message.Section = LogSection.ContentPackList;
|
message.Section = LogSection.ContentPackList;
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,21 +48,24 @@ namespace StardewModdingAPI.Web.Framework.LogParsing.Models
|
||||||
[MemberNotNullWhen(true, nameof(LogModInfo.UpdateVersion), nameof(LogModInfo.UpdateLink))]
|
[MemberNotNullWhen(true, nameof(LogModInfo.UpdateVersion), nameof(LogModInfo.UpdateLink))]
|
||||||
public bool HasUpdate => this.UpdateVersion != null && this.Version != this.UpdateVersion;
|
public bool HasUpdate => this.UpdateVersion != null && this.Version != this.UpdateVersion;
|
||||||
|
|
||||||
|
/// <summary>The mod type.</summary>
|
||||||
|
public ModType ModType { get; }
|
||||||
|
|
||||||
/// <summary>Whether this is an actual mod (rather than a special entry for SMAPI or the game itself).</summary>
|
/// <summary>Whether this is an actual mod (rather than a special entry for SMAPI or the game itself).</summary>
|
||||||
public bool IsMod { get; }
|
public bool IsMod => this.ModType != ModType.Special;
|
||||||
|
|
||||||
/// <summary>Whether this is a C# code mod.</summary>
|
/// <summary>Whether this is a C# code mod.</summary>
|
||||||
public bool IsCodeMod { get; }
|
public bool IsCodeMod => this.ModType == ModType.CodeMod;
|
||||||
|
|
||||||
/// <summary>Whether this is a content pack for another mod.</summary>
|
/// <summary>Whether this is a content pack for another mod.</summary>
|
||||||
[MemberNotNullWhen(true, nameof(LogModInfo.ContentPackFor))]
|
public bool IsContentPack => this.ModType == ModType.ContentPack;
|
||||||
public bool IsContentPack { get; }
|
|
||||||
|
|
||||||
|
|
||||||
/*********
|
/*********
|
||||||
** Public methods
|
** Public methods
|
||||||
*********/
|
*********/
|
||||||
/// <summary>Construct an instance.</summary>
|
/// <summary>Construct an instance.</summary>
|
||||||
|
/// <param name="modType">The mod type.</param>
|
||||||
/// <param name="name">The mod name.</param>
|
/// <param name="name">The mod name.</param>
|
||||||
/// <param name="author">The mod author.</param>
|
/// <param name="author">The mod author.</param>
|
||||||
/// <param name="version">The mod version.</param>
|
/// <param name="version">The mod version.</param>
|
||||||
|
@ -72,9 +75,9 @@ namespace StardewModdingAPI.Web.Framework.LogParsing.Models
|
||||||
/// <param name="contentPackFor">The name of the mod for which this is a content pack (if applicable).</param>
|
/// <param name="contentPackFor">The name of the mod for which this is a content pack (if applicable).</param>
|
||||||
/// <param name="errors">The number of errors logged by this mod.</param>
|
/// <param name="errors">The number of errors logged by this mod.</param>
|
||||||
/// <param name="loaded">Whether the mod was loaded into the game.</param>
|
/// <param name="loaded">Whether the mod was loaded into the game.</param>
|
||||||
/// <param name="isMod">Whether this is an actual mod (instead of a special entry for SMAPI or the game).</param>
|
public LogModInfo(ModType modType, string name, string author, string version, string description, string? updateVersion = null, string? updateLink = null, string? contentPackFor = null, int errors = 0, bool loaded = true)
|
||||||
public LogModInfo(string name, string author, string version, string description, string? updateVersion = null, string? updateLink = null, string? contentPackFor = null, int errors = 0, bool loaded = true, bool isMod = true)
|
|
||||||
{
|
{
|
||||||
|
this.ModType = modType;
|
||||||
this.Name = name;
|
this.Name = name;
|
||||||
this.Author = author;
|
this.Author = author;
|
||||||
this.Description = description;
|
this.Description = description;
|
||||||
|
@ -84,13 +87,6 @@ namespace StardewModdingAPI.Web.Framework.LogParsing.Models
|
||||||
this.Errors = errors;
|
this.Errors = errors;
|
||||||
this.Loaded = loaded;
|
this.Loaded = loaded;
|
||||||
|
|
||||||
if (isMod)
|
|
||||||
{
|
|
||||||
this.IsMod = true;
|
|
||||||
this.IsContentPack = !string.IsNullOrWhiteSpace(this.ContentPackFor);
|
|
||||||
this.IsCodeMod = !this.IsContentPack;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.OverrideVersion(version);
|
this.OverrideVersion(version);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
namespace StardewModdingAPI.Web.Framework.LogParsing.Models
|
||||||
|
{
|
||||||
|
/// <summary>The type for a <see cref="LogModInfo"/> instance.</summary>
|
||||||
|
public enum ModType
|
||||||
|
{
|
||||||
|
/// <summary>A special non-mod entry (e.g. for SMAPI or the game itself).</summary>
|
||||||
|
Special,
|
||||||
|
|
||||||
|
/// <summary>A C# mod.</summary>
|
||||||
|
CodeMod,
|
||||||
|
|
||||||
|
/// <summary>A content pack loaded by a C# mod.</summary>
|
||||||
|
ContentPack
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,7 +15,7 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Azure.Storage.Blobs" Version="12.12.0" />
|
<PackageReference Include="Azure.Storage.Blobs" Version="12.13.0" />
|
||||||
<PackageReference Include="Hangfire.AspNetCore" Version="1.7.29" />
|
<PackageReference Include="Hangfire.AspNetCore" Version="1.7.29" />
|
||||||
<PackageReference Include="Hangfire.MemoryStorage" Version="1.7.0" />
|
<PackageReference Include="Hangfire.MemoryStorage" Version="1.7.0" />
|
||||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.43" />
|
<PackageReference Include="HtmlAgilityPack" Version="1.11.43" />
|
||||||
|
|
|
@ -107,7 +107,7 @@ namespace StardewModdingAPI.Web.ViewModels
|
||||||
// group by mod
|
// group by mod
|
||||||
return mods
|
return mods
|
||||||
.Where(mod => mod.IsContentPack)
|
.Where(mod => mod.IsContentPack)
|
||||||
.GroupBy(mod => mod.ContentPackFor!)
|
.GroupBy(mod => mod.ContentPackFor ?? "")
|
||||||
.ToDictionary(group => group.Key, group => group.ToArray());
|
.ToDictionary(group => group.Key, group => group.ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,12 +10,12 @@
|
||||||
@section Head {
|
@section Head {
|
||||||
<link rel="stylesheet" href="~/Content/css/index.css" />
|
<link rel="stylesheet" href="~/Content/css/index.css" />
|
||||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1" crossorigin="anonymous"></script>
|
<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1" crossorigin="anonymous"></script>
|
||||||
<script src="~/Content/js/index.js"></script>
|
<script src="~/Content/js/index.js?r=20220708"></script>
|
||||||
}
|
}
|
||||||
|
|
||||||
<h1>
|
<h1>
|
||||||
SMAPI
|
SMAPI
|
||||||
<img id="pufferchick" src="Content/images/pufferchick.png" />
|
<img id="pufferchick" src="Content/images/pufferchick.svg" />
|
||||||
</h1>
|
</h1>
|
||||||
<div id="blurb">
|
<div id="blurb">
|
||||||
<p>The mod loader for Stardew Valley.</p>
|
<p>The mod loader for Stardew Valley.</p>
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
LogModInfo[] outdatedMods = log?.Mods.Where(mod => mod.HasUpdate).ToArray() ?? Array.Empty<LogModInfo>();
|
LogModInfo[] outdatedMods = log?.Mods.Where(mod => mod.HasUpdate).ToArray() ?? Array.Empty<LogModInfo>();
|
||||||
LogModInfo? errorHandler = log?.Mods.FirstOrDefault(p => p.IsCodeMod && p.Name == "Error Handler");
|
LogModInfo? errorHandler = log?.Mods.FirstOrDefault(p => p.IsCodeMod && p.Name == "Error Handler");
|
||||||
bool hasOlderErrorHandler = errorHandler?.GetParsedVersion() is not null && log?.ApiVersionParsed is not null && log.ApiVersionParsed.IsNewerThan(errorHandler.GetParsedVersion());
|
bool hasOlderErrorHandler = errorHandler?.GetParsedVersion() is not null && log?.ApiVersionParsed is not null && log.ApiVersionParsed.IsNewerThan(errorHandler.GetParsedVersion());
|
||||||
|
bool isPyTkCompatibilityMode = log?.ApiVersionParsed?.IsOlderThan("3.15.0") is false && log.Mods.Any(p => p.IsCodeMod && p.Name == "PyTK" && p.GetParsedVersion()?.IsOlderThan("1.23.1") is true);
|
||||||
|
|
||||||
// get filters
|
// get filters
|
||||||
IDictionary<string, bool> defaultFilters = Enum
|
IDictionary<string, bool> defaultFilters = Enum
|
||||||
|
@ -242,7 +243,7 @@ else if (log?.IsValid == true)
|
||||||
@if (log?.IsValid == true)
|
@if (log?.IsValid == true)
|
||||||
{
|
{
|
||||||
<div id="output">
|
<div id="output">
|
||||||
@if (outdatedMods.Any() || errorHandler is null || hasOlderErrorHandler)
|
@if (outdatedMods.Any() || errorHandler is null || hasOlderErrorHandler || isPyTkCompatibilityMode)
|
||||||
{
|
{
|
||||||
<h2>Suggested fixes</h2>
|
<h2>Suggested fixes</h2>
|
||||||
<ul id="fix-list">
|
<ul id="fix-list">
|
||||||
|
@ -254,6 +255,10 @@ else if (log?.IsValid == true)
|
||||||
{
|
{
|
||||||
<li>Your <strong>Error Handler</strong> mod is older than SMAPI. You may be missing some game/mod error fixes. You can <a href="https://stardewvalleywiki.com/Modding:Player_Guide#Install_SMAPI">reinstall SMAPI</a> to update it.</li>
|
<li>Your <strong>Error Handler</strong> mod is older than SMAPI. You may be missing some game/mod error fixes. You can <a href="https://stardewvalleywiki.com/Modding:Player_Guide#Install_SMAPI">reinstall SMAPI</a> to update it.</li>
|
||||||
}
|
}
|
||||||
|
@if (isPyTkCompatibilityMode)
|
||||||
|
{
|
||||||
|
<li>PyTK 1.23.0 or earlier isn't compatible with newer SMAPI performance optimizations. This may increase loading times or in-game lag.</li>
|
||||||
|
}
|
||||||
@if (outdatedMods.Any())
|
@if (outdatedMods.Any())
|
||||||
{
|
{
|
||||||
<li>
|
<li>
|
||||||
|
@ -347,16 +352,31 @@ else if (log?.IsValid == true)
|
||||||
<span class="notice btn txt" v-on:click="toggleContentPacks">toggle content packs in list</span>
|
<span class="notice btn txt" v-on:click="toggleContentPacks">toggle content packs in list</span>
|
||||||
}
|
}
|
||||||
</caption>
|
</caption>
|
||||||
@foreach (var mod in log.Mods.Where(p => p.Loaded && !p.IsContentPack))
|
|
||||||
{
|
|
||||||
if (contentPacks == null || !contentPacks.TryGetValue(mod.Name, out LogModInfo[]? contentPackList))
|
|
||||||
contentPackList = null;
|
|
||||||
|
|
||||||
|
@{
|
||||||
|
var modsWithContentPacks = log.Mods
|
||||||
|
.Where(mod => mod.Loaded && !mod.IsContentPack)
|
||||||
|
.Select(mod => (
|
||||||
|
Mod: mod,
|
||||||
|
ContentPacks: contentPacks?.TryGetValue(mod.Name, out LogModInfo[]? contentPackList) == true ? contentPackList : Array.Empty<LogModInfo>()
|
||||||
|
))
|
||||||
|
.ToList();
|
||||||
|
if (contentPacks?.TryGetValue("", out LogModInfo[] invalidPacks) == true)
|
||||||
|
{
|
||||||
|
modsWithContentPacks.Add((
|
||||||
|
Mod: new LogModInfo(ModType.CodeMod, "<invalid content packs>", "", "", ""),
|
||||||
|
ContentPacks: invalidPacks
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@foreach ((LogModInfo mod, LogModInfo[] contentPackList) in modsWithContentPacks)
|
||||||
|
{
|
||||||
<tr v-on:click="toggleMod('@Model.GetSlug(mod.Name)')" class="mod-entry" v-bind:class="{ hidden: !showMods['@Model.GetSlug(mod.Name)'] }">
|
<tr v-on:click="toggleMod('@Model.GetSlug(mod.Name)')" class="mod-entry" v-bind:class="{ hidden: !showMods['@Model.GetSlug(mod.Name)'] }">
|
||||||
<td><input type="checkbox" v-bind:checked="showMods['@Model.GetSlug(mod.Name)']" v-bind:class="{ invisible: !anyModsHidden }" /></td>
|
<td><input type="checkbox" v-bind:checked="showMods['@Model.GetSlug(mod.Name)']" v-bind:class="{ invisible: !anyModsHidden }" /></td>
|
||||||
<td>
|
<td>
|
||||||
<strong v-pre>@mod.Name</strong> @mod.Version
|
<strong v-pre>@mod.Name</strong> @mod.Version
|
||||||
@if (contentPackList != null)
|
@if (contentPackList.Any())
|
||||||
{
|
{
|
||||||
<div v-if="!hideContentPacks" class="content-packs">
|
<div v-if="!hideContentPacks" class="content-packs">
|
||||||
@foreach (var contentPack in contentPackList)
|
@foreach (var contentPack in contentPackList)
|
||||||
|
@ -369,7 +389,7 @@ else if (log?.IsValid == true)
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@mod.Author
|
@mod.Author
|
||||||
@if (contentPackList != null)
|
@if (contentPackList.Any())
|
||||||
{
|
{
|
||||||
<div v-if="!hideContentPacks" class="content-packs">
|
<div v-if="!hideContentPacks" class="content-packs">
|
||||||
@foreach (var contentPack in contentPackList)
|
@foreach (var contentPack in contentPackList)
|
||||||
|
|
|
@ -68,7 +68,7 @@ a {
|
||||||
margin-top: 3em;
|
margin-top: 3em;
|
||||||
min-height: 75%;
|
min-height: 75%;
|
||||||
width: 12em;
|
width: 12em;
|
||||||
background: url("../images/sidebar-bg.gif") no-repeat top right;
|
background: url("../images/sidebar-bg.svg") no-repeat top right;
|
||||||
color: #666;
|
color: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 1.1 KiB |
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 6.4 KiB |
Binary file not shown.
Before Width: | Height: | Size: 831 B |
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 5.7 KiB |
Binary file not shown.
Before Width: | Height: | Size: 1.1 KiB |
|
@ -0,0 +1 @@
|
||||||
|
<svg width="63" height="126" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#a)"><path d="M63.002 30.06c-18.36 0-33.297 14.778-33.297 32.94 0 18.163 14.938 32.94 33.297 32.94 18.359 0 33.295-14.776 33.295-32.94 0-18.162-14.936-32.94-33.295-32.94ZM115.603 63c0-1.674-.088-3.327-.244-4.96L126 53.78a61.838 61.838 0 0 0-2.182-9.52l-11.451.73a51.898 51.898 0 0 0-4.358-8.944l7.708-8.395a63.075 63.075 0 0 0-6.159-7.63L99.556 25.6a52.684 52.684 0 0 0-7.83-6.186l3.27-10.892a63.833 63.833 0 0 0-8.886-4.243l-6.57 9.323a52.763 52.763 0 0 0-9.774-2.198L67.934.187a65.106 65.106 0 0 0-9.87 0l-1.83 11.216A52.795 52.795 0 0 0 46.46 13.6l-6.568-9.323a63.782 63.782 0 0 0-8.886 4.243l3.271 10.892a52.659 52.659 0 0 0-7.832 6.186l-10.001-5.576a63.235 63.235 0 0 0-6.16 7.629l7.71 8.395a51.7 51.7 0 0 0-4.359 8.945l-11.452-.73A62.05 62.05 0 0 0 0 53.78l10.642 4.258a51.832 51.832 0 0 0-.245 4.96c0 1.673.088 3.326.245 4.96L0 72.217a62.178 62.178 0 0 0 2.182 9.52l11.452-.731a51.65 51.65 0 0 0 4.36 8.944l-7.711 8.396a63.311 63.311 0 0 0 6.16 7.629l10-5.576a52.731 52.731 0 0 0 7.833 6.186l-3.271 10.892a63.789 63.789 0 0 0 8.886 4.242l6.568-9.321a52.772 52.772 0 0 0 9.775 2.198l1.83 11.216c3.285.248 6.585.248 9.87 0l1.832-11.216a52.76 52.76 0 0 0 9.774-2.198l6.569 9.321a63.767 63.767 0 0 0 8.886-4.242l-3.269-10.892a52.904 52.904 0 0 0 7.83-6.186l10.002 5.576a63.1 63.1 0 0 0 6.159-7.63l-7.711-8.395a51.625 51.625 0 0 0 4.361-8.944l11.451.73a61.714 61.714 0 0 0 2.182-9.52l-10.641-4.258c.156-1.633.244-3.286.244-4.96V63Zm-52.601 38.253c-21.322 0-38.67-17.16-38.67-38.252 0-21.093 17.348-38.253 38.67-38.253 21.321 0 38.668 17.16 38.668 38.253s-17.347 38.252-38.668 38.252Z" fill="#C4C4C4"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h63v126H0z"/></clipPath></defs></svg>
|
After Width: | Height: | Size: 1.7 KiB |
|
@ -3,10 +3,10 @@ $(document).ready(function () {
|
||||||
var pufferchick = $("#pufferchick");
|
var pufferchick = $("#pufferchick");
|
||||||
$(".cta-dropdown").hover(
|
$(".cta-dropdown").hover(
|
||||||
function () {
|
function () {
|
||||||
pufferchick.attr("src", "Content/images/pufferchick-cool.png");
|
pufferchick.attr("src", "Content/images/pufferchick-cool.svg");
|
||||||
},
|
},
|
||||||
function () {
|
function () {
|
||||||
pufferchick.attr("src", "Content/images/pufferchick.png");
|
pufferchick.attr("src", "Content/images/pufferchick.svg");
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -14,9 +14,9 @@
|
||||||
"title": "Format version",
|
"title": "Format version",
|
||||||
"description": "The format version. You should always use the latest version to enable the latest features, avoid obsolete behavior, and reduce load times.",
|
"description": "The format version. You should always use the latest version to enable the latest features, avoid obsolete behavior, and reduce load times.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"pattern": "^1\\.27\\.[0-9]+$",
|
"pattern": "^1\\.28\\.[0-9]+$",
|
||||||
"@errorMessages": {
|
"@errorMessages": {
|
||||||
"pattern": "Incorrect value '@value'. You should always use the latest format version (currently 1.27.0) to enable the latest features, avoid obsolete behavior, and reduce load times."
|
"pattern": "Incorrect value '@value'. You should always use the latest format version (currently 1.28.0) to enable the latest features, avoid obsolete behavior, and reduce load times."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ConfigSchema": {
|
"ConfigSchema": {
|
||||||
|
|
|
@ -52,7 +52,7 @@ namespace StardewModdingAPI
|
||||||
internal static int? LogScreenId { get; set; }
|
internal static int? LogScreenId { get; set; }
|
||||||
|
|
||||||
/// <summary>SMAPI's current raw semantic version.</summary>
|
/// <summary>SMAPI's current raw semantic version.</summary>
|
||||||
internal static string RawApiVersion = "3.15.1";
|
internal static string RawApiVersion = "3.16.0";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Contains SMAPI's constants and assumptions.</summary>
|
/// <summary>Contains SMAPI's constants and assumptions.</summary>
|
||||||
|
@ -90,7 +90,7 @@ namespace StardewModdingAPI
|
||||||
source: null,
|
source: null,
|
||||||
nounPhrase: $"{nameof(Constants)}.{nameof(Constants.ExecutionPath)}",
|
nounPhrase: $"{nameof(Constants)}.{nameof(Constants.ExecutionPath)}",
|
||||||
version: "3.14.0",
|
version: "3.14.0",
|
||||||
severity: DeprecationLevel.Notice
|
severity: DeprecationLevel.Info
|
||||||
);
|
);
|
||||||
|
|
||||||
return Constants.GamePath;
|
return Constants.GamePath;
|
||||||
|
@ -244,8 +244,8 @@ namespace StardewModdingAPI
|
||||||
internal static void ConfigureAssemblyResolver(AssemblyDefinitionResolver resolver)
|
internal static void ConfigureAssemblyResolver(AssemblyDefinitionResolver resolver)
|
||||||
{
|
{
|
||||||
// add search paths
|
// add search paths
|
||||||
resolver.AddSearchDirectory(Constants.GamePath);
|
resolver.TryAddSearchDirectory(Constants.GamePath);
|
||||||
resolver.AddSearchDirectory(Constants.InternalFilesPath);
|
resolver.TryAddSearchDirectory(Constants.InternalFilesPath);
|
||||||
|
|
||||||
// add SMAPI explicitly
|
// add SMAPI explicitly
|
||||||
// Normally this would be handled automatically by the search paths, but for some reason there's a specific
|
// Normally this would be handled automatically by the search paths, but for some reason there's a specific
|
||||||
|
|
|
@ -45,7 +45,7 @@ namespace StardewModdingAPI.Framework.Content
|
||||||
source: null,
|
source: null,
|
||||||
nounPhrase: $"{nameof(IAssetInfo)}.{nameof(IAssetInfo.AssetName)}",
|
nounPhrase: $"{nameof(IAssetInfo)}.{nameof(IAssetInfo.AssetName)}",
|
||||||
version: "3.14.0",
|
version: "3.14.0",
|
||||||
severity: DeprecationLevel.Notice,
|
severity: DeprecationLevel.Info,
|
||||||
unlessStackIncludes: new[]
|
unlessStackIncludes: new[]
|
||||||
{
|
{
|
||||||
$"{typeof(AssetInterceptorChange).FullName}.{nameof(AssetInterceptorChange.CanIntercept)}",
|
$"{typeof(AssetInterceptorChange).FullName}.{nameof(AssetInterceptorChange.CanIntercept)}",
|
||||||
|
@ -84,7 +84,7 @@ namespace StardewModdingAPI.Framework.Content
|
||||||
source: null,
|
source: null,
|
||||||
nounPhrase: $"{nameof(IAssetInfo)}.{nameof(IAssetInfo.AssetNameEquals)}",
|
nounPhrase: $"{nameof(IAssetInfo)}.{nameof(IAssetInfo.AssetNameEquals)}",
|
||||||
version: "3.14.0",
|
version: "3.14.0",
|
||||||
severity: DeprecationLevel.Notice,
|
severity: DeprecationLevel.Info,
|
||||||
unlessStackIncludes: new[]
|
unlessStackIncludes: new[]
|
||||||
{
|
{
|
||||||
$"{typeof(AssetInterceptorChange).FullName}.{nameof(AssetInterceptorChange.CanIntercept)}",
|
$"{typeof(AssetInterceptorChange).FullName}.{nameof(AssetInterceptorChange.CanIntercept)}",
|
||||||
|
|
|
@ -254,6 +254,10 @@ namespace StardewModdingAPI.Framework.ContentManagers
|
||||||
{
|
{
|
||||||
using FileStream stream = File.OpenRead(file.FullName);
|
using FileStream stream = File.OpenRead(file.FullName);
|
||||||
using SKBitmap bitmap = SKBitmap.Decode(stream);
|
using SKBitmap bitmap = SKBitmap.Decode(stream);
|
||||||
|
|
||||||
|
if (bitmap is null)
|
||||||
|
throw new InvalidDataException($"Failed to load {file.FullName}. This doesn't seem to be a valid PNG image.");
|
||||||
|
|
||||||
rawPixels = SKPMColor.PreMultiply(bitmap.Pixels);
|
rawPixels = SKPMColor.PreMultiply(bitmap.Pixels);
|
||||||
width = bitmap.Width;
|
width = bitmap.Width;
|
||||||
height = bitmap.Height;
|
height = bitmap.Height;
|
||||||
|
|
|
@ -124,7 +124,7 @@ namespace StardewModdingAPI.Framework.Deprecations
|
||||||
}
|
}
|
||||||
|
|
||||||
// log message
|
// log message
|
||||||
if (level == LogLevel.Trace)
|
if (level is LogLevel.Trace or LogLevel.Debug)
|
||||||
{
|
{
|
||||||
if (warning.LogStackTrace)
|
if (warning.LogStackTrace)
|
||||||
message += $"\n{this.GetSimplifiedStackTrace(warning.StackTrace, warning.Mod)}";
|
message += $"\n{this.GetSimplifiedStackTrace(warning.StackTrace, warning.Mod)}";
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
namespace StardewModdingAPI.Framework
|
||||||
|
{
|
||||||
|
/// <summary>The SMAPI exit state.</summary>
|
||||||
|
internal enum ExitState
|
||||||
|
{
|
||||||
|
/// <summary>SMAPI didn't trigger an explicit exit.</summary>
|
||||||
|
None,
|
||||||
|
|
||||||
|
/// <summary>The game is exiting normally.</summary>
|
||||||
|
GameExit,
|
||||||
|
|
||||||
|
/// <summary>The game is exiting due to an error.</summary>
|
||||||
|
Crash
|
||||||
|
}
|
||||||
|
}
|
|
@ -43,7 +43,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||||
source: this.Mod,
|
source: this.Mod,
|
||||||
nounPhrase: $"{nameof(IModHelper)}.{nameof(IModHelper.ConsoleCommands)}.{nameof(ICommandHelper.Trigger)}",
|
nounPhrase: $"{nameof(IModHelper)}.{nameof(IModHelper.ConsoleCommands)}.{nameof(ICommandHelper.Trigger)}",
|
||||||
version: "3.8.1",
|
version: "3.8.1",
|
||||||
severity: DeprecationLevel.Notice
|
severity: DeprecationLevel.Info
|
||||||
);
|
);
|
||||||
|
|
||||||
return this.CommandManager.Trigger(name, arguments);
|
return this.CommandManager.Trigger(name, arguments);
|
||||||
|
|
|
@ -62,7 +62,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||||
source: this.Mod,
|
source: this.Mod,
|
||||||
nounPhrase: $"{nameof(IContentHelper)}.{nameof(IContentHelper.AssetLoaders)}",
|
nounPhrase: $"{nameof(IContentHelper)}.{nameof(IContentHelper.AssetLoaders)}",
|
||||||
version: "3.14.0",
|
version: "3.14.0",
|
||||||
severity: DeprecationLevel.Notice
|
severity: DeprecationLevel.Info
|
||||||
);
|
);
|
||||||
|
|
||||||
return this.ObservableAssetLoaders;
|
return this.ObservableAssetLoaders;
|
||||||
|
@ -78,7 +78,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||||
source: this.Mod,
|
source: this.Mod,
|
||||||
nounPhrase: $"{nameof(IContentHelper)}.{nameof(IContentHelper.AssetEditors)}",
|
nounPhrase: $"{nameof(IContentHelper)}.{nameof(IContentHelper.AssetEditors)}",
|
||||||
version: "3.14.0",
|
version: "3.14.0",
|
||||||
severity: DeprecationLevel.Notice
|
severity: DeprecationLevel.Info
|
||||||
);
|
);
|
||||||
|
|
||||||
return this.ObservableAssetEditors;
|
return this.ObservableAssetEditors;
|
||||||
|
@ -126,7 +126,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||||
this.Mod,
|
this.Mod,
|
||||||
"loading assets from the Content folder with a .xnb file extension",
|
"loading assets from the Content folder with a .xnb file extension",
|
||||||
"3.14.0",
|
"3.14.0",
|
||||||
DeprecationLevel.Notice
|
DeprecationLevel.Info
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,7 +150,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||||
this.Mod,
|
this.Mod,
|
||||||
"loading XNB files from the mod folder without the .xnb file extension",
|
"loading XNB files from the mod folder without the .xnb file extension",
|
||||||
"3.14.0",
|
"3.14.0",
|
||||||
DeprecationLevel.Notice
|
DeprecationLevel.Info
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||||
source: this.Mod,
|
source: this.Mod,
|
||||||
nounPhrase: $"{nameof(IModHelper)}.{nameof(IModHelper.Content)}",
|
nounPhrase: $"{nameof(IModHelper)}.{nameof(IModHelper.Content)}",
|
||||||
version: "3.14.0",
|
version: "3.14.0",
|
||||||
severity: DeprecationLevel.Notice
|
severity: DeprecationLevel.Info
|
||||||
);
|
);
|
||||||
|
|
||||||
return this.ContentImpl;
|
return this.ContentImpl;
|
||||||
|
|
|
@ -4,18 +4,31 @@ using Mono.Cecil;
|
||||||
namespace StardewModdingAPI.Framework.ModLoading
|
namespace StardewModdingAPI.Framework.ModLoading
|
||||||
{
|
{
|
||||||
/// <summary>A minimal assembly definition resolver which resolves references to known assemblies.</summary>
|
/// <summary>A minimal assembly definition resolver which resolves references to known assemblies.</summary>
|
||||||
internal class AssemblyDefinitionResolver : DefaultAssemblyResolver
|
internal class AssemblyDefinitionResolver : IAssemblyResolver
|
||||||
{
|
{
|
||||||
/*********
|
/*********
|
||||||
** Fields
|
** Fields
|
||||||
*********/
|
*********/
|
||||||
|
/// <summary>The underlying assembly resolver.</summary>
|
||||||
|
private readonly DefaultAssemblyResolverWrapper Resolver = new();
|
||||||
|
|
||||||
/// <summary>The known assemblies.</summary>
|
/// <summary>The known assemblies.</summary>
|
||||||
private readonly IDictionary<string, AssemblyDefinition> Lookup = new Dictionary<string, AssemblyDefinition>();
|
private readonly IDictionary<string, AssemblyDefinition> Lookup = new Dictionary<string, AssemblyDefinition>();
|
||||||
|
|
||||||
|
/// <summary>The directory paths to search for assemblies.</summary>
|
||||||
|
private readonly HashSet<string> SearchPaths = new();
|
||||||
|
|
||||||
|
|
||||||
/*********
|
/*********
|
||||||
** Public methods
|
** Public methods
|
||||||
*********/
|
*********/
|
||||||
|
/// <summary>Construct an instance.</summary>
|
||||||
|
public AssemblyDefinitionResolver()
|
||||||
|
{
|
||||||
|
foreach (string path in this.Resolver.GetSearchDirectories())
|
||||||
|
this.SearchPaths.Add(path);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Add known assemblies to the resolver.</summary>
|
/// <summary>Add known assemblies to the resolver.</summary>
|
||||||
/// <param name="assemblies">The known assemblies.</param>
|
/// <param name="assemblies">The known assemblies.</param>
|
||||||
public void Add(params AssemblyDefinition[] assemblies)
|
public void Add(params AssemblyDefinition[] assemblies)
|
||||||
|
@ -29,7 +42,7 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||||
/// <param name="names">The assembly names for which it should be returned.</param>
|
/// <param name="names">The assembly names for which it should be returned.</param>
|
||||||
public void AddWithExplicitNames(AssemblyDefinition assembly, params string[] names)
|
public void AddWithExplicitNames(AssemblyDefinition assembly, params string[] names)
|
||||||
{
|
{
|
||||||
this.RegisterAssembly(assembly);
|
this.Resolver.AddAssembly(assembly);
|
||||||
foreach (string name in names)
|
foreach (string name in names)
|
||||||
this.Lookup[name] = assembly;
|
this.Lookup[name] = assembly;
|
||||||
}
|
}
|
||||||
|
@ -37,18 +50,52 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||||
/// <summary>Resolve an assembly reference.</summary>
|
/// <summary>Resolve an assembly reference.</summary>
|
||||||
/// <param name="name">The assembly name.</param>
|
/// <param name="name">The assembly name.</param>
|
||||||
/// <exception cref="AssemblyResolutionException">The assembly can't be resolved.</exception>
|
/// <exception cref="AssemblyResolutionException">The assembly can't be resolved.</exception>
|
||||||
public override AssemblyDefinition Resolve(AssemblyNameReference name)
|
public AssemblyDefinition Resolve(AssemblyNameReference name)
|
||||||
{
|
{
|
||||||
return this.ResolveName(name.Name) ?? base.Resolve(name);
|
return this.ResolveName(name.Name) ?? this.Resolver.Resolve(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Resolve an assembly reference.</summary>
|
/// <summary>Resolve an assembly reference.</summary>
|
||||||
/// <param name="name">The assembly name.</param>
|
/// <param name="name">The assembly name.</param>
|
||||||
/// <param name="parameters">The assembly reader parameters.</param>
|
/// <param name="parameters">The assembly reader parameters.</param>
|
||||||
/// <exception cref="AssemblyResolutionException">The assembly can't be resolved.</exception>
|
/// <exception cref="AssemblyResolutionException">The assembly can't be resolved.</exception>
|
||||||
public override AssemblyDefinition Resolve(AssemblyNameReference name, ReaderParameters parameters)
|
public AssemblyDefinition Resolve(AssemblyNameReference name, ReaderParameters parameters)
|
||||||
{
|
{
|
||||||
return this.ResolveName(name.Name) ?? base.Resolve(name, parameters);
|
return this.ResolveName(name.Name) ?? this.Resolver.Resolve(name, parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Add a directory path to search for assemblies, if it's non-null and not already added.</summary>
|
||||||
|
/// <param name="path">The path to search.</param>
|
||||||
|
/// <returns>Returns whether the path was successfully added.</returns>
|
||||||
|
public bool TryAddSearchDirectory(string? path)
|
||||||
|
{
|
||||||
|
if (path is not null && this.SearchPaths.Add(path))
|
||||||
|
{
|
||||||
|
this.Resolver.AddSearchDirectory(path);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Remove a directory path to search for assemblies, if it's non-null.</summary>
|
||||||
|
/// <param name="path">The path to remove.</param>
|
||||||
|
/// <returns>Returns whether the path was in the list and removed.</returns>
|
||||||
|
public bool RemoveSearchDirectory(string? path)
|
||||||
|
{
|
||||||
|
if (path is not null && this.SearchPaths.Remove(path))
|
||||||
|
{
|
||||||
|
this.Resolver.RemoveSearchDirectory(path);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
this.Resolver.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -63,5 +110,16 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||||
? match
|
? match
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>An internal wrapper around <see cref="DefaultAssemblyResolver"/> to allow access to its protected methods.</summary>
|
||||||
|
private class DefaultAssemblyResolverWrapper : DefaultAssemblyResolver
|
||||||
|
{
|
||||||
|
/// <summary>Add an assembly to the resolver.</summary>
|
||||||
|
/// <param name="assembly">The assembly to add.</param>
|
||||||
|
public void AddAssembly(AssemblyDefinition assembly)
|
||||||
|
{
|
||||||
|
this.RegisterAssembly(assembly);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -264,8 +264,15 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||||
if (!file.Exists)
|
if (!file.Exists)
|
||||||
yield break; // not a local assembly
|
yield break; // not a local assembly
|
||||||
|
|
||||||
|
// add the assembly's directory temporarily if needed
|
||||||
|
// this is needed by F# mods which bundle FSharp.Core.dll, for example
|
||||||
|
string? temporarySearchDir = null;
|
||||||
|
if (this.AssemblyDefinitionResolver.TryAddSearchDirectory(file.DirectoryName))
|
||||||
|
temporarySearchDir = file.DirectoryName;
|
||||||
|
|
||||||
// read assembly
|
// read assembly
|
||||||
AssemblyDefinition assembly;
|
AssemblyDefinition assembly;
|
||||||
|
try
|
||||||
{
|
{
|
||||||
byte[] assemblyBytes = File.ReadAllBytes(file.FullName);
|
byte[] assemblyBytes = File.ReadAllBytes(file.FullName);
|
||||||
Stream readStream = this.TrackForDisposal(new MemoryStream(assemblyBytes));
|
Stream readStream = this.TrackForDisposal(new MemoryStream(assemblyBytes));
|
||||||
|
@ -286,6 +293,12 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||||
assembly = this.TrackForDisposal(AssemblyDefinition.ReadAssembly(readStream, new ReaderParameters(ReadingMode.Immediate) { AssemblyResolver = assemblyResolver, InMemory = true }));
|
assembly = this.TrackForDisposal(AssemblyDefinition.ReadAssembly(readStream, new ReaderParameters(ReadingMode.Immediate) { AssemblyResolver = assemblyResolver, InMemory = true }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// clean up temporary search directory
|
||||||
|
if (temporarySearchDir is not null)
|
||||||
|
this.AssemblyDefinitionResolver.RemoveSearchDirectory(temporarySearchDir);
|
||||||
|
}
|
||||||
|
|
||||||
// skip if already visited
|
// skip if already visited
|
||||||
if (visitedAssemblyNames.Contains(assembly.Name.Name))
|
if (visitedAssemblyNames.Contains(assembly.Name.Name))
|
||||||
|
|
|
@ -47,7 +47,7 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||||
IModMetadata metadata = new ModMetadata(folder.DisplayName, folder.Directory.FullName, rootPath, manifest, dataRecord, isIgnored: shouldIgnore);
|
IModMetadata metadata = new ModMetadata(folder.DisplayName, folder.Directory.FullName, rootPath, manifest, dataRecord, isIgnored: shouldIgnore);
|
||||||
if (shouldIgnore)
|
if (shouldIgnore)
|
||||||
metadata.SetStatus(status, ModFailReason.DisabledByDotConvention, "disabled by dot convention");
|
metadata.SetStatus(status, ModFailReason.DisabledByDotConvention, "disabled by dot convention");
|
||||||
else
|
else if (status == ModMetadataStatus.Failed)
|
||||||
metadata.SetStatus(status, ModFailReason.InvalidManifest, folder.ManifestParseErrorText);
|
metadata.SetStatus(status, ModFailReason.InvalidManifest, folder.ManifestParseErrorText);
|
||||||
|
|
||||||
yield return metadata;
|
yield return metadata;
|
||||||
|
@ -223,8 +223,8 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||||
{
|
{
|
||||||
foreach (IModMetadata mod in group)
|
foreach (IModMetadata mod in group)
|
||||||
{
|
{
|
||||||
if (mod.Status == ModMetadataStatus.Failed)
|
if (mod.Status == ModMetadataStatus.Failed && mod.FailReason != ModFailReason.InvalidManifest)
|
||||||
continue; // don't replace metadata error
|
continue;
|
||||||
|
|
||||||
string folderList = string.Join(", ", group.Select(p => p.GetRelativePathWithRoot()).OrderBy(p => p));
|
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}.");
|
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}.");
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
@ -50,6 +49,7 @@ using StardewModdingAPI.Utilities;
|
||||||
using StardewValley;
|
using StardewValley;
|
||||||
using StardewValley.Menus;
|
using StardewValley.Menus;
|
||||||
using StardewValley.Objects;
|
using StardewValley.Objects;
|
||||||
|
using StardewValley.SDKs;
|
||||||
using xTile.Display;
|
using xTile.Display;
|
||||||
using LanguageCode = StardewValley.LocalizedContentManager.LanguageCode;
|
using LanguageCode = StardewValley.LocalizedContentManager.LanguageCode;
|
||||||
using MiniMonoModHotfix = MonoMod.Utils.MiniMonoModHotfix;
|
using MiniMonoModHotfix = MonoMod.Utils.MiniMonoModHotfix;
|
||||||
|
@ -67,8 +67,11 @@ namespace StardewModdingAPI.Framework
|
||||||
/****
|
/****
|
||||||
** Low-level components
|
** Low-level components
|
||||||
****/
|
****/
|
||||||
|
/// <summary>A state which indicates whether SMAPI should exit immediately and any pending initialization should be cancelled.</summary>
|
||||||
|
private ExitState ExitState;
|
||||||
|
|
||||||
/// <summary>Whether the game should exit immediately and any pending initialization should be cancelled.</summary>
|
/// <summary>Whether the game should exit immediately and any pending initialization should be cancelled.</summary>
|
||||||
private bool IsExiting;
|
private bool IsExiting => this.ExitState != ExitState.None;
|
||||||
|
|
||||||
/// <summary>Manages the SMAPI console window and log file.</summary>
|
/// <summary>Manages the SMAPI console window and log file.</summary>
|
||||||
private readonly LogManager LogManager;
|
private readonly LogManager LogManager;
|
||||||
|
@ -297,22 +300,13 @@ namespace StardewModdingAPI.Framework
|
||||||
this.IsGameRunning = true;
|
this.IsGameRunning = true;
|
||||||
StardewValley.Program.releaseBuild = true; // game's debug logic interferes with SMAPI opening the game window
|
StardewValley.Program.releaseBuild = true; // game's debug logic interferes with SMAPI opening the game window
|
||||||
this.Game.Run();
|
this.Game.Run();
|
||||||
|
this.Dispose(isError: false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
this.LogManager.LogFatalLaunchError(ex);
|
this.LogManager.LogFatalLaunchError(ex);
|
||||||
this.LogManager.PressAnyKeyToExit();
|
this.LogManager.PressAnyKeyToExit();
|
||||||
}
|
this.Dispose(isError: true);
|
||||||
finally
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
this.Dispose();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
this.Monitor.Log($"The game ended, but SMAPI wasn't able to dispose correctly. Technical details: {ex}", LogLevel.Error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -327,6 +321,14 @@ namespace StardewModdingAPI.Framework
|
||||||
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
|
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
|
||||||
[SuppressMessage("ReSharper", "ConditionalAccessQualifierIsNonNullableAccordingToAPIContract", Justification = "May be disposed before SMAPI is fully initialized.")]
|
[SuppressMessage("ReSharper", "ConditionalAccessQualifierIsNonNullableAccordingToAPIContract", Justification = "May be disposed before SMAPI is fully initialized.")]
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
{
|
||||||
|
this.Dispose(isError: true); // if we got here, SMAPI didn't detect the exit before it happened
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
|
||||||
|
/// <param name="isError">Whether the process is exiting due to an error or crash.</param>
|
||||||
|
[SuppressMessage("ReSharper", "ConditionalAccessQualifierIsNonNullableAccordingToAPIContract", Justification = "May be disposed before SMAPI is fully initialized.")]
|
||||||
|
public void Dispose(bool isError)
|
||||||
{
|
{
|
||||||
// skip if already disposed
|
// skip if already disposed
|
||||||
if (this.IsDisposed)
|
if (this.IsDisposed)
|
||||||
|
@ -349,13 +351,29 @@ namespace StardewModdingAPI.Framework
|
||||||
|
|
||||||
// dispose core components
|
// dispose core components
|
||||||
this.IsGameRunning = false;
|
this.IsGameRunning = false;
|
||||||
this.IsExiting = true;
|
if (this.ExitState == ExitState.None || isError)
|
||||||
|
this.ExitState = isError ? ExitState.Crash : ExitState.GameExit;
|
||||||
this.ContentCore?.Dispose();
|
this.ContentCore?.Dispose();
|
||||||
this.Game?.Dispose();
|
this.Game?.Dispose();
|
||||||
this.LogManager.Dispose(); // dispose last to allow for any last-second log messages
|
this.LogManager.Dispose(); // dispose last to allow for any last-second log messages
|
||||||
|
|
||||||
// end game (moved from Game1.OnExiting to let us clean up first)
|
// clean up SDK
|
||||||
Process.GetCurrentProcess().Kill();
|
// This avoids Steam connection errors when it exits unexpectedly. The game avoids this
|
||||||
|
// by killing the entire process, but we can't set the error code if we do that.
|
||||||
|
try
|
||||||
|
{
|
||||||
|
FieldInfo? field = typeof(StardewValley.Program).GetField("_sdk", BindingFlags.NonPublic | BindingFlags.Static);
|
||||||
|
SDKHelper? sdk = field?.GetValue(null) as SDKHelper;
|
||||||
|
sdk?.Shutdown();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// well, at least we tried
|
||||||
|
}
|
||||||
|
|
||||||
|
// end game with error code
|
||||||
|
// This helps the OS decide whether to keep the window open (e.g. Windows may keep it open on error).
|
||||||
|
Environment.Exit(this.ExitState == ExitState.Crash ? 1 : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -387,8 +405,15 @@ namespace StardewModdingAPI.Framework
|
||||||
{
|
{
|
||||||
string[] looseFiles = new DirectoryInfo(this.ModsPath).GetFiles().Select(p => p.Name).ToArray();
|
string[] looseFiles = new DirectoryInfo(this.ModsPath).GetFiles().Select(p => p.Name).ToArray();
|
||||||
if (looseFiles.Any())
|
if (looseFiles.Any())
|
||||||
|
{
|
||||||
|
if (looseFiles.Any(name => name.Equals("manifest.json", StringComparison.OrdinalIgnoreCase) || name.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
this.Monitor.Log($"Detected mod files directly inside the '{Path.GetFileName(this.ModsPath)}' folder. These will be ignored. Each mod must have its own subfolder instead.", LogLevel.Error);
|
||||||
|
}
|
||||||
|
|
||||||
this.Monitor.Log($" Ignored loose files: {string.Join(", ", looseFiles.OrderBy(p => p, StringComparer.OrdinalIgnoreCase))}");
|
this.Monitor.Log($" Ignored loose files: {string.Join(", ", looseFiles.OrderBy(p => p, StringComparer.OrdinalIgnoreCase))}");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// load manifests
|
// load manifests
|
||||||
IModMetadata[] mods = resolver.ReadManifests(toolkit, this.ModsPath, modDatabase, useCaseInsensitiveFilePaths: this.Settings.UseCaseInsensitivePaths).ToArray();
|
IModMetadata[] mods = resolver.ReadManifests(toolkit, this.ModsPath, modDatabase, useCaseInsensitiveFilePaths: this.Settings.UseCaseInsensitivePaths).ToArray();
|
||||||
|
@ -1250,7 +1275,7 @@ namespace StardewModdingAPI.Framework
|
||||||
private void OnGameExiting()
|
private void OnGameExiting()
|
||||||
{
|
{
|
||||||
this.Multiplayer.Disconnect(StardewValley.Multiplayer.DisconnectType.ClosedGame);
|
this.Multiplayer.Disconnect(StardewValley.Multiplayer.DisconnectType.ClosedGame);
|
||||||
this.Dispose();
|
this.Dispose(isError: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Raised when a mod network message is received.</summary>
|
/// <summary>Raised when a mod network message is received.</summary>
|
||||||
|
@ -1670,7 +1695,7 @@ namespace StardewModdingAPI.Framework
|
||||||
source: metadata,
|
source: metadata,
|
||||||
nounPhrase: $"{nameof(IAssetEditor)}",
|
nounPhrase: $"{nameof(IAssetEditor)}",
|
||||||
version: "3.14.0",
|
version: "3.14.0",
|
||||||
severity: DeprecationLevel.Notice,
|
severity: DeprecationLevel.Info,
|
||||||
logStackTrace: false
|
logStackTrace: false
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1683,7 +1708,7 @@ namespace StardewModdingAPI.Framework
|
||||||
source: metadata,
|
source: metadata,
|
||||||
nounPhrase: $"{nameof(IAssetLoader)}",
|
nounPhrase: $"{nameof(IAssetLoader)}",
|
||||||
version: "3.14.0",
|
version: "3.14.0",
|
||||||
severity: DeprecationLevel.Notice,
|
severity: DeprecationLevel.Info,
|
||||||
logStackTrace: false
|
logStackTrace: false
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1715,7 +1740,7 @@ namespace StardewModdingAPI.Framework
|
||||||
metadata,
|
metadata,
|
||||||
$"using {name} without bundling it",
|
$"using {name} without bundling it",
|
||||||
"3.14.7",
|
"3.14.7",
|
||||||
DeprecationLevel.Notice,
|
DeprecationLevel.Info,
|
||||||
logStackTrace: false
|
logStackTrace: false
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2239,7 +2264,7 @@ namespace StardewModdingAPI.Framework
|
||||||
this.Monitor.LogFatal(message);
|
this.Monitor.LogFatal(message);
|
||||||
this.LogManager.WriteCrashLog();
|
this.LogManager.WriteCrashLog();
|
||||||
|
|
||||||
this.IsExiting = true;
|
this.ExitState = ExitState.Crash;
|
||||||
this.Game.Exit();
|
this.Game.Exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
<!-- tiered compilation breaks Harmony -->
|
<!-- tiered compilation breaks Harmony -->
|
||||||
<TieredCompilation>false</TieredCompilation>
|
<TieredCompilation>false</TieredCompilation>
|
||||||
|
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<Import Project="..\..\build\common.targets" />
|
<Import Project="..\..\build\common.targets" />
|
||||||
|
@ -26,7 +27,7 @@
|
||||||
<PackageReference Include="MonoMod.Common" Version="22.3.5.1" />
|
<PackageReference Include="MonoMod.Common" Version="22.3.5.1" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||||
<PackageReference Include="Pathoschild.Http.FluentClient" Version="4.1.1" />
|
<PackageReference Include="Pathoschild.Http.FluentClient" Version="4.1.1" />
|
||||||
<PackageReference Include="Pintail" Version="2.2.0" />
|
<PackageReference Include="Pintail" Version="2.2.1" />
|
||||||
<PackageReference Include="Platonymous.TMXTile" Version="1.5.9" />
|
<PackageReference Include="Platonymous.TMXTile" Version="1.5.9" />
|
||||||
<PackageReference Include="System.Reflection.Emit" Version="4.7.0" />
|
<PackageReference Include="System.Reflection.Emit" Version="4.7.0" />
|
||||||
|
|
||||||
|
|
|
@ -59,7 +59,7 @@ namespace StardewModdingAPI.Utilities
|
||||||
null,
|
null,
|
||||||
$"calling the {nameof(PerScreen<T>)} constructor with null",
|
$"calling the {nameof(PerScreen<T>)} constructor with null",
|
||||||
"3.14.0",
|
"3.14.0",
|
||||||
DeprecationLevel.Notice
|
DeprecationLevel.Info
|
||||||
);
|
);
|
||||||
#else
|
#else
|
||||||
throw new ArgumentNullException(nameof(createNewState));
|
throw new ArgumentNullException(nameof(createNewState));
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<assemblyIdentity version="1.0.0.0" name="StardropEngine"/>
|
||||||
|
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||||
|
<security>
|
||||||
|
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||||
|
</requestedPrivileges>
|
||||||
|
</security>
|
||||||
|
</trustInfo>
|
||||||
|
|
||||||
|
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||||
|
<application>
|
||||||
|
<!-- A list of the Windows versions that this application has been tested on and is
|
||||||
|
is designed to work with. Uncomment the appropriate elements and Windows will
|
||||||
|
automatically selected the most compatible environment. -->
|
||||||
|
|
||||||
|
<!-- Windows Vista -->
|
||||||
|
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />
|
||||||
|
|
||||||
|
<!-- Windows 7 -->
|
||||||
|
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />
|
||||||
|
|
||||||
|
<!-- Windows 8 -->
|
||||||
|
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
|
||||||
|
|
||||||
|
<!-- Windows 8.1 -->
|
||||||
|
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
|
||||||
|
|
||||||
|
<!-- Windows 10 -->
|
||||||
|
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||||
|
</application>
|
||||||
|
</compatibility>
|
||||||
|
|
||||||
|
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<windowsSettings>
|
||||||
|
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
|
||||||
|
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness>
|
||||||
|
</windowsSettings>
|
||||||
|
</application>
|
||||||
|
</assembly>
|
|
@ -2,5 +2,5 @@
|
||||||
// short date format for SDate
|
// short date format for SDate
|
||||||
// tokens: {{day}} (like 15), {{season}} (like Spring), {{seasonLowercase}} (like spring), {{year}} (like 2)
|
// tokens: {{day}} (like 15), {{season}} (like Spring), {{seasonLowercase}} (like spring), {{year}} (like 2)
|
||||||
"generic.date": "{{day}} {{season}}",
|
"generic.date": "{{day}} {{season}}",
|
||||||
"generic.date-with-year": "{{day}} {{season}} года {{year}}"
|
"generic.date-with-year": "{{day}} {{season}} Yıl {{year}}"
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue