diff --git a/.gitignore b/.gitignore index 527ac6bb..a89ccd21 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,6 @@ appsettings.Development.json # Azure generated files src/SMAPI.Web/Properties/PublishProfiles/*.pubxml src/SMAPI.Web/Properties/ServiceDependencies/* - Web Deploy/ + +# macOS +.DS_Store diff --git a/build/common.targets b/build/common.targets index 230bef41..383e258b 100644 --- a/build/common.targets +++ b/build/common.targets @@ -7,12 +7,11 @@ repo. It imports the other MSBuild files as needed. - 3.15.1 + 3.16.0 SMAPI latest $(AssemblySearchPaths);{GAC} $(DefineConstants);SMAPI_DEPRECATED - pdbonly true diff --git a/docs/release-notes.md b/docs/release-notes.md index 8b258fb3..ca7d6e6e 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,9 +4,39 @@ +## 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 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: * 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)); - * 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). * 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. diff --git a/docs/technical/mod-package.md b/docs/technical/mod-package.md index dd65a992..ca78be55 100644 --- a/docs/technical/mod-package.md +++ b/docs/technical/mod-package.md @@ -412,6 +412,9 @@ The NuGet package is generated automatically in `StardewModdingAPI.ModBuildConfi when you compile it. ## Release notes +### Upcoming release +* Switched to the newer crossplatform `portable` debug symbols (thanks to lanturnalis!). + ### 4.0.1 Released 14 April 2022. diff --git a/docs/technical/smapi.md b/docs/technical/smapi.md index 90990ee4..b8a1683b 100644 --- a/docs/technical/smapi.md +++ b/docs/technical/smapi.md @@ -33,14 +33,15 @@ argument | purpose `--uninstall` | Preselects the uninstall action, skipping the prompt asking what the user wants to do. `--game-path "path"` | Specifies the full path to the folder containing the Stardew Valley executable, skipping automatic detection and any prompt to choose a path. If the path is not valid, the installer displays an error. -SMAPI itself recognises five arguments **on Windows only**, but these are intended for internal use -or testing and may change without warning. On Linux/macOS, see _environment variables_ below. +SMAPI itself recognises five arguments, but these are meant for internal use or testing, and might +change without warning. **On Linux/macOS**, command-line arguments won't work; see _environment +variables_ below instead. argument | purpose -------- | ------- `--developer-mode`
`--developer-mode-off` | Enable or disable features intended for mod developers. Currently this only makes `TRACE`-level messages appear in the console. -`--no-terminal` | The SMAPI launcher won't try to open a terminal window, and SMAPI won't log anything to the console. (Messages will still be written to the log file.) -`--use-current-shell` | The SMAPI launcher won't try to open a terminal window, but SMAPI will still log to the console. (Messages will still be written to the log file.) +`--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` | 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. ### Environment variables diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index fd1a6047..d00a5df4 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -206,7 +206,7 @@ namespace StardewModdingApi.Installer Console.WriteLine(); // 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) { case "1": @@ -629,22 +629,22 @@ namespace StardewModdingApi.Installer } /// Interactively ask the user to choose a value. - /// A callback which prints a message to the console. + /// A callback which prints a message to the console. /// The message to print. /// The allowed options (not case sensitive). /// The indentation to prefix to output. - private string InteractivelyChoose(string message, string[] options, string indent = "", Action? print = null) + private string InteractivelyChoose(string message, string[] options, string indent = "", Action? printLine = null) { - print ??= this.PrintInfo; + printLine ??= this.PrintInfo; while (true) { - print(indent + message); + printLine(indent + message); Console.Write(indent); string? input = Console.ReadLine()?.Trim().ToLowerInvariant(); if (input == null || !options.Contains(input)) { - print($"{indent}That's not a valid option."); + printLine($"{indent}That's not a valid option."); continue; } return input; diff --git a/src/SMAPI.Installer/assets/unix-launcher.sh b/src/SMAPI.Installer/assets/unix-launcher.sh index ae9624e7..778663d7 100644 --- a/src/SMAPI.Installer/assets/unix-launcher.sh +++ b/src/SMAPI.Installer/assets/unix-launcher.sh @@ -54,12 +54,12 @@ if [ "$(uname)" == "Darwin" ]; then # https://stackoverflow.com/a/29511052/262123 if [ "$USE_CURRENT_SHELL" == "false" ]; then echo "Reopening in the Terminal app..." - echo '#!/bin/sh' > /tmp/open-smapi-terminal.sh - echo "\"$0\" $@ --use-current-shell" >> /tmp/open-smapi-terminal.sh - chmod +x /tmp/open-smapi-terminal.sh - cat /tmp/open-smapi-terminal.sh - open -W -a Terminal /tmp/open-smapi-terminal.sh - rm /tmp/open-smapi-terminal.sh + echo '#!/bin/sh' > /tmp/open-smapi-terminal.command + echo "\"$0\" $@ --use-current-shell" >> /tmp/open-smapi-terminal.command + chmod +x /tmp/open-smapi-terminal.command + cat /tmp/open-smapi-terminal.command + open -W /tmp/open-smapi-terminal.command + rm /tmp/open-smapi-terminal.command exit 0 fi fi @@ -71,8 +71,8 @@ fi ########## # script must be run from the game folder 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"; - read + printf "Oops! SMAPI must be placed in the Stardew Valley game folder.\nSee instructions: https://stardewvalleywiki.com/Modding:Player_Guide"; + read -r exit 1 fi @@ -102,37 +102,39 @@ else # find the true shell behind x-terminal-emulator 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 # run in selected terminal and account for quirks - export TERMINAL_PATH="$(command -v $TERMINAL_NAME)" - if [ -x $TERMINAL_PATH ]; then + TERMINAL_PATH="$(command -v "$TERMINAL_NAME")" + export TERMINAL_PATH + if [ -x "$TERMINAL_PATH" ]; then case $TERMINAL_NAME in terminal|termite) # consumes only one argument after -e # 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) # 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) # 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) # consumes all arguments after -- - exec $TERMINAL_NAME -- env TERM=xterm $LAUNCH_FILE "$@" + exec "$TERMINAL_NAME" -- env TERM=xterm $LAUNCH_FILE "$@" ;; kitty) # consumes all trailing arguments - exec $TERMINAL_NAME env TERM=xterm $LAUNCH_FILE "$@" + exec "$TERMINAL_NAME" env TERM=xterm $LAUNCH_FILE "$@" ;; *) diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets index b66ec27b..12619439 100644 --- a/src/SMAPI.ModBuildConfig/build/smapi.targets +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -8,8 +8,7 @@ ** Set build options **********************************************--> - - pdbonly + true diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index 3c2dec19..adc45ec3 100644 --- a/src/SMAPI.Mods.ConsoleCommands/manifest.json +++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json @@ -1,9 +1,9 @@ { "Name": "Console Commands", "Author": "SMAPI", - "Version": "3.15.1", + "Version": "3.16.0", "Description": "Adds SMAPI console commands that let you manipulate the game.", "UniqueID": "SMAPI.ConsoleCommands", "EntryDll": "ConsoleCommands.dll", - "MinimumApiVersion": "3.15.1" + "MinimumApiVersion": "3.16.0" } diff --git a/src/SMAPI.Mods.ErrorHandler/manifest.json b/src/SMAPI.Mods.ErrorHandler/manifest.json index 28b4b149..3a3c9283 100644 --- a/src/SMAPI.Mods.ErrorHandler/manifest.json +++ b/src/SMAPI.Mods.ErrorHandler/manifest.json @@ -1,9 +1,9 @@ { "Name": "Error Handler", "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.", "UniqueID": "SMAPI.ErrorHandler", "EntryDll": "ErrorHandler.dll", - "MinimumApiVersion": "3.15.1" + "MinimumApiVersion": "3.16.0" } diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json index 1944575b..eb98aa32 100644 --- a/src/SMAPI.Mods.SaveBackup/manifest.json +++ b/src/SMAPI.Mods.SaveBackup/manifest.json @@ -1,9 +1,9 @@ { "Name": "Save Backup", "Author": "SMAPI", - "Version": "3.15.1", + "Version": "3.16.0", "Description": "Automatically backs up all your saves once per day into its folder.", "UniqueID": "SMAPI.SaveBackup", "EntryDll": "SaveBackup.dll", - "MinimumApiVersion": "3.15.1" + "MinimumApiVersion": "3.16.0" } diff --git a/src/SMAPI.Web/Controllers/LogParserController.cs b/src/SMAPI.Web/Controllers/LogParserController.cs index 33af5a81..a3bcf4c3 100644 --- a/src/SMAPI.Web/Controllers/LogParserController.cs +++ b/src/SMAPI.Web/Controllers/LogParserController.cs @@ -1,7 +1,9 @@ using System; -using System.Linq; +using System.Collections.Specialized; +using System.IO; using System.Text; using System.Threading.Tasks; +using System.Web; using Microsoft.AspNetCore.Mvc; using StardewModdingAPI.Toolkit.Utilities; using StardewModdingAPI.Web.Framework; @@ -87,9 +89,15 @@ namespace StardewModdingAPI.Web.Controllers public async Task PostAsync() { // get raw log text - string? input = this.Request.Form["input"].FirstOrDefault(); - if (string.IsNullOrWhiteSpace(input)) - return this.View("Index", this.GetModel(null, uploadError: "The log file seems to be empty.")); + // 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)) + return this.View("Index", this.GetModel(null, uploadError: "The log file seems to be empty.")); + } // upload log UploadResult uploadResult = await this.Storage.SaveAsync(input); diff --git a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs index 0efa62c5..18ba754c 100644 --- a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs +++ b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs @@ -36,13 +36,13 @@ namespace StardewModdingAPI.Web.Framework.LogParsing private readonly Regex ContentPackListStartPattern = new(@"^Loaded \d+ content packs:$", RegexOptions.Compiled | RegexOptions.IgnoreCase); /// A regex pattern matching an entry in SMAPI's content pack list. - private readonly Regex ContentPackListEntryPattern = new(@"^ (?.+?) (?[^\s]+)(?: by (?[^\|]+))? \| for (?[^\|]+)(?: \| (?.+))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private readonly Regex ContentPackListEntryPattern = new(@"^ (?.+?) (?[^\s]+)(?: by (?[^\|]+))? \| for (?[^\|]*)(?: \| (?.+))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase); /// A regex pattern matching the start of SMAPI's mod update list. private readonly Regex ModUpdateListStartPattern = new(@"^You can update \d+ mods?:$", RegexOptions.Compiled | RegexOptions.IgnoreCase); /// A regex pattern matching an entry in SMAPI's mod update list. - private readonly Regex ModUpdateListEntryPattern = new(@"^ (?.+) (?[^\s]+): (?.+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private readonly Regex ModUpdateListEntryPattern = new(@"^ (?.+) (?[^\s]+): (?[^\s]+)(?: \(you have [^\)]+\))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase); /// A regex pattern matching SMAPI's update line. private readonly Regex SmapiUpdatePattern = new(@"^You can update SMAPI to (?[^\s]+): (?.+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); @@ -77,8 +77,8 @@ namespace StardewModdingAPI.Web.Framework.LogParsing }; // parse log messages - LogModInfo smapiMod = new(name: "SMAPI", author: "Pathoschild", version: "", description: "", loaded: true, isMod: false); - LogModInfo gameMod = new(name: "game", author: "", version: "", description: "", loaded: true, isMod: false); + LogModInfo smapiMod = new(ModType.Special, name: "SMAPI", author: "Pathoschild", version: "", description: "", loaded: true); + LogModInfo gameMod = new(ModType.Special, name: "game", author: "", version: "", description: "", loaded: true); IDictionary> mods = new Dictionary>(); bool inModList = false; bool inContentPackList = false; @@ -133,7 +133,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing if (!mods.TryGetValue(name, out List? entries)) mods[name] = entries = new List(); - 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; } @@ -156,7 +156,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing if (!mods.TryGetValue(name, out List? entries)) mods[name] = entries = new List(); - 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; } diff --git a/src/SMAPI.Web/Framework/LogParsing/Models/LogModInfo.cs b/src/SMAPI.Web/Framework/LogParsing/Models/LogModInfo.cs index 557f08ff..c81942e4 100644 --- a/src/SMAPI.Web/Framework/LogParsing/Models/LogModInfo.cs +++ b/src/SMAPI.Web/Framework/LogParsing/Models/LogModInfo.cs @@ -48,21 +48,24 @@ namespace StardewModdingAPI.Web.Framework.LogParsing.Models [MemberNotNullWhen(true, nameof(LogModInfo.UpdateVersion), nameof(LogModInfo.UpdateLink))] public bool HasUpdate => this.UpdateVersion != null && this.Version != this.UpdateVersion; + /// The mod type. + public ModType ModType { get; } + /// Whether this is an actual mod (rather than a special entry for SMAPI or the game itself). - public bool IsMod { get; } + public bool IsMod => this.ModType != ModType.Special; /// Whether this is a C# code mod. - public bool IsCodeMod { get; } + public bool IsCodeMod => this.ModType == ModType.CodeMod; /// Whether this is a content pack for another mod. - [MemberNotNullWhen(true, nameof(LogModInfo.ContentPackFor))] - public bool IsContentPack { get; } + public bool IsContentPack => this.ModType == ModType.ContentPack; /********* ** Public methods *********/ /// Construct an instance. + /// The mod type. /// The mod name. /// The mod author. /// The mod version. @@ -72,9 +75,9 @@ namespace StardewModdingAPI.Web.Framework.LogParsing.Models /// The name of the mod for which this is a content pack (if applicable). /// The number of errors logged by this mod. /// Whether the mod was loaded into the game. - /// Whether this is an actual mod (instead of a special entry for SMAPI or the game). - 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) + 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) { + this.ModType = modType; this.Name = name; this.Author = author; this.Description = description; @@ -84,13 +87,6 @@ namespace StardewModdingAPI.Web.Framework.LogParsing.Models this.Errors = errors; this.Loaded = loaded; - if (isMod) - { - this.IsMod = true; - this.IsContentPack = !string.IsNullOrWhiteSpace(this.ContentPackFor); - this.IsCodeMod = !this.IsContentPack; - } - this.OverrideVersion(version); } diff --git a/src/SMAPI.Web/Framework/LogParsing/Models/ModType.cs b/src/SMAPI.Web/Framework/LogParsing/Models/ModType.cs new file mode 100644 index 00000000..363aaaec --- /dev/null +++ b/src/SMAPI.Web/Framework/LogParsing/Models/ModType.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Web.Framework.LogParsing.Models +{ + /// The type for a instance. + public enum ModType + { + /// A special non-mod entry (e.g. for SMAPI or the game itself). + Special, + + /// A C# mod. + CodeMod, + + /// A content pack loaded by a C# mod. + ContentPack + } +} diff --git a/src/SMAPI.Web/SMAPI.Web.csproj b/src/SMAPI.Web/SMAPI.Web.csproj index 0a460e9e..d26cb6f8 100644 --- a/src/SMAPI.Web/SMAPI.Web.csproj +++ b/src/SMAPI.Web/SMAPI.Web.csproj @@ -15,7 +15,7 @@ - + diff --git a/src/SMAPI.Web/ViewModels/LogParserModel.cs b/src/SMAPI.Web/ViewModels/LogParserModel.cs index c39a9b0a..34d0b4df 100644 --- a/src/SMAPI.Web/ViewModels/LogParserModel.cs +++ b/src/SMAPI.Web/ViewModels/LogParserModel.cs @@ -107,7 +107,7 @@ namespace StardewModdingAPI.Web.ViewModels // group by mod return mods .Where(mod => mod.IsContentPack) - .GroupBy(mod => mod.ContentPackFor!) + .GroupBy(mod => mod.ContentPackFor ?? "") .ToDictionary(group => group.Key, group => group.ToArray()); } diff --git a/src/SMAPI.Web/Views/Index/Index.cshtml b/src/SMAPI.Web/Views/Index/Index.cshtml index acb8df78..d6472fcb 100644 --- a/src/SMAPI.Web/Views/Index/Index.cshtml +++ b/src/SMAPI.Web/Views/Index/Index.cshtml @@ -10,12 +10,12 @@ @section Head { - + }

SMAPI - +

The mod loader for Stardew Valley.

diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index b824b768..f71c6ac1 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -17,6 +17,7 @@ LogModInfo[] outdatedMods = log?.Mods.Where(mod => mod.HasUpdate).ToArray() ?? Array.Empty(); 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 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 IDictionary defaultFilters = Enum @@ -242,7 +243,7 @@ else if (log?.IsValid == true) @if (log?.IsValid == true) {
- @if (outdatedMods.Any() || errorHandler is null || hasOlderErrorHandler) + @if (outdatedMods.Any() || errorHandler is null || hasOlderErrorHandler || isPyTkCompatibilityMode) {

Suggested fixes

    @@ -254,6 +255,10 @@ else if (log?.IsValid == true) {
  • Your Error Handler mod is older than SMAPI. You may be missing some game/mod error fixes. You can reinstall SMAPI to update it.
  • } + @if (isPyTkCompatibilityMode) + { +
  • PyTK 1.23.0 or earlier isn't compatible with newer SMAPI performance optimizations. This may increase loading times or in-game lag.
  • + } @if (outdatedMods.Any()) {
  • @@ -347,16 +352,31 @@ else if (log?.IsValid == true) toggle content packs in list } - @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() + )) + .ToList(); + if (contentPacks?.TryGetValue("", out LogModInfo[] invalidPacks) == true) + { + modsWithContentPacks.Add(( + Mod: new LogModInfo(ModType.CodeMod, "", "", "", ""), + ContentPacks: invalidPacks + )); + } + } + + @foreach ((LogModInfo mod, LogModInfo[] contentPackList) in modsWithContentPacks) + { @mod.Name @mod.Version - @if (contentPackList != null) + @if (contentPackList.Any()) {
    @foreach (var contentPack in contentPackList) @@ -369,7 +389,7 @@ else if (log?.IsValid == true) @mod.Author - @if (contentPackList != null) + @if (contentPackList.Any()) {
    @foreach (var contentPack in contentPackList) diff --git a/src/SMAPI.Web/wwwroot/Content/css/main.css b/src/SMAPI.Web/wwwroot/Content/css/main.css index a0a407d8..79de9c39 100644 --- a/src/SMAPI.Web/wwwroot/Content/css/main.css +++ b/src/SMAPI.Web/wwwroot/Content/css/main.css @@ -68,7 +68,7 @@ a { margin-top: 3em; min-height: 75%; width: 12em; - background: url("../images/sidebar-bg.gif") no-repeat top right; + background: url("../images/sidebar-bg.svg") no-repeat top right; color: #666; } diff --git a/src/SMAPI.Web/wwwroot/Content/images/pufferchick-cool.png b/src/SMAPI.Web/wwwroot/Content/images/pufferchick-cool.png deleted file mode 100644 index f359146c..00000000 Binary files a/src/SMAPI.Web/wwwroot/Content/images/pufferchick-cool.png and /dev/null differ diff --git a/src/SMAPI.Web/wwwroot/Content/images/pufferchick-cool.svg b/src/SMAPI.Web/wwwroot/Content/images/pufferchick-cool.svg new file mode 100644 index 00000000..2734c289 --- /dev/null +++ b/src/SMAPI.Web/wwwroot/Content/images/pufferchick-cool.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/SMAPI.Web/wwwroot/Content/images/pufferchick.png b/src/SMAPI.Web/wwwroot/Content/images/pufferchick.png deleted file mode 100644 index 1de9cf47..00000000 Binary files a/src/SMAPI.Web/wwwroot/Content/images/pufferchick.png and /dev/null differ diff --git a/src/SMAPI.Web/wwwroot/Content/images/pufferchick.svg b/src/SMAPI.Web/wwwroot/Content/images/pufferchick.svg new file mode 100644 index 00000000..5e77e493 --- /dev/null +++ b/src/SMAPI.Web/wwwroot/Content/images/pufferchick.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/SMAPI.Web/wwwroot/Content/images/sidebar-bg.gif b/src/SMAPI.Web/wwwroot/Content/images/sidebar-bg.gif deleted file mode 100644 index 48e9af5a..00000000 Binary files a/src/SMAPI.Web/wwwroot/Content/images/sidebar-bg.gif and /dev/null differ diff --git a/src/SMAPI.Web/wwwroot/Content/images/sidebar-bg.svg b/src/SMAPI.Web/wwwroot/Content/images/sidebar-bg.svg new file mode 100644 index 00000000..f0613272 --- /dev/null +++ b/src/SMAPI.Web/wwwroot/Content/images/sidebar-bg.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/SMAPI.Web/wwwroot/Content/js/index.js b/src/SMAPI.Web/wwwroot/Content/js/index.js index d0734b02..04db5c2f 100644 --- a/src/SMAPI.Web/wwwroot/Content/js/index.js +++ b/src/SMAPI.Web/wwwroot/Content/js/index.js @@ -3,10 +3,10 @@ $(document).ready(function () { var pufferchick = $("#pufferchick"); $(".cta-dropdown").hover( function () { - pufferchick.attr("src", "Content/images/pufferchick-cool.png"); + pufferchick.attr("src", "Content/images/pufferchick-cool.svg"); }, function () { - pufferchick.attr("src", "Content/images/pufferchick.png"); + pufferchick.attr("src", "Content/images/pufferchick.svg"); } ); diff --git a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json index 631fbc63..a00403c0 100644 --- a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json +++ b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json @@ -14,9 +14,9 @@ "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.", "type": "string", - "pattern": "^1\\.27\\.[0-9]+$", + "pattern": "^1\\.28\\.[0-9]+$", "@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": { diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index d733e61e..c79a72ef 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -52,7 +52,7 @@ namespace StardewModdingAPI internal static int? LogScreenId { get; set; } /// SMAPI's current raw semantic version. - internal static string RawApiVersion = "3.15.1"; + internal static string RawApiVersion = "3.16.0"; } /// Contains SMAPI's constants and assumptions. @@ -90,7 +90,7 @@ namespace StardewModdingAPI source: null, nounPhrase: $"{nameof(Constants)}.{nameof(Constants.ExecutionPath)}", version: "3.14.0", - severity: DeprecationLevel.Notice + severity: DeprecationLevel.Info ); return Constants.GamePath; @@ -244,8 +244,8 @@ namespace StardewModdingAPI internal static void ConfigureAssemblyResolver(AssemblyDefinitionResolver resolver) { // add search paths - resolver.AddSearchDirectory(Constants.GamePath); - resolver.AddSearchDirectory(Constants.InternalFilesPath); + resolver.TryAddSearchDirectory(Constants.GamePath); + resolver.TryAddSearchDirectory(Constants.InternalFilesPath); // add SMAPI explicitly // Normally this would be handled automatically by the search paths, but for some reason there's a specific diff --git a/src/SMAPI/Framework/Content/AssetInfo.cs b/src/SMAPI/Framework/Content/AssetInfo.cs index 43feed27..af000300 100644 --- a/src/SMAPI/Framework/Content/AssetInfo.cs +++ b/src/SMAPI/Framework/Content/AssetInfo.cs @@ -45,7 +45,7 @@ namespace StardewModdingAPI.Framework.Content source: null, nounPhrase: $"{nameof(IAssetInfo)}.{nameof(IAssetInfo.AssetName)}", version: "3.14.0", - severity: DeprecationLevel.Notice, + severity: DeprecationLevel.Info, unlessStackIncludes: new[] { $"{typeof(AssetInterceptorChange).FullName}.{nameof(AssetInterceptorChange.CanIntercept)}", @@ -84,7 +84,7 @@ namespace StardewModdingAPI.Framework.Content source: null, nounPhrase: $"{nameof(IAssetInfo)}.{nameof(IAssetInfo.AssetNameEquals)}", version: "3.14.0", - severity: DeprecationLevel.Notice, + severity: DeprecationLevel.Info, unlessStackIncludes: new[] { $"{typeof(AssetInterceptorChange).FullName}.{nameof(AssetInterceptorChange.CanIntercept)}", diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index f3cf05d9..8ecbc4cc 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -254,6 +254,10 @@ namespace StardewModdingAPI.Framework.ContentManagers { using FileStream stream = File.OpenRead(file.FullName); 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); width = bitmap.Width; height = bitmap.Height; diff --git a/src/SMAPI/Framework/Deprecations/DeprecationManager.cs b/src/SMAPI/Framework/Deprecations/DeprecationManager.cs index 4597a764..f58f085e 100644 --- a/src/SMAPI/Framework/Deprecations/DeprecationManager.cs +++ b/src/SMAPI/Framework/Deprecations/DeprecationManager.cs @@ -124,7 +124,7 @@ namespace StardewModdingAPI.Framework.Deprecations } // log message - if (level == LogLevel.Trace) + if (level is LogLevel.Trace or LogLevel.Debug) { if (warning.LogStackTrace) message += $"\n{this.GetSimplifiedStackTrace(warning.StackTrace, warning.Mod)}"; diff --git a/src/SMAPI/Framework/ExitState.cs b/src/SMAPI/Framework/ExitState.cs new file mode 100644 index 00000000..bc022d91 --- /dev/null +++ b/src/SMAPI/Framework/ExitState.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Framework +{ + /// The SMAPI exit state. + internal enum ExitState + { + /// SMAPI didn't trigger an explicit exit. + None, + + /// The game is exiting normally. + GameExit, + + /// The game is exiting due to an error. + Crash + } +} diff --git a/src/SMAPI/Framework/ModHelpers/CommandHelper.cs b/src/SMAPI/Framework/ModHelpers/CommandHelper.cs index 21435f62..b7d4861f 100644 --- a/src/SMAPI/Framework/ModHelpers/CommandHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/CommandHelper.cs @@ -43,7 +43,7 @@ namespace StardewModdingAPI.Framework.ModHelpers source: this.Mod, nounPhrase: $"{nameof(IModHelper)}.{nameof(IModHelper.ConsoleCommands)}.{nameof(ICommandHelper.Trigger)}", version: "3.8.1", - severity: DeprecationLevel.Notice + severity: DeprecationLevel.Info ); return this.CommandManager.Trigger(name, arguments); diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs index 9992cb52..0a1633bf 100644 --- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs @@ -62,7 +62,7 @@ namespace StardewModdingAPI.Framework.ModHelpers source: this.Mod, nounPhrase: $"{nameof(IContentHelper)}.{nameof(IContentHelper.AssetLoaders)}", version: "3.14.0", - severity: DeprecationLevel.Notice + severity: DeprecationLevel.Info ); return this.ObservableAssetLoaders; @@ -78,7 +78,7 @@ namespace StardewModdingAPI.Framework.ModHelpers source: this.Mod, nounPhrase: $"{nameof(IContentHelper)}.{nameof(IContentHelper.AssetEditors)}", version: "3.14.0", - severity: DeprecationLevel.Notice + severity: DeprecationLevel.Info ); return this.ObservableAssetEditors; @@ -126,7 +126,7 @@ namespace StardewModdingAPI.Framework.ModHelpers this.Mod, "loading assets from the Content folder with a .xnb file extension", "3.14.0", - DeprecationLevel.Notice + DeprecationLevel.Info ); } @@ -150,7 +150,7 @@ namespace StardewModdingAPI.Framework.ModHelpers this.Mod, "loading XNB files from the mod folder without the .xnb file extension", "3.14.0", - DeprecationLevel.Notice + DeprecationLevel.Info ); return data; } diff --git a/src/SMAPI/Framework/ModHelpers/ModHelper.cs b/src/SMAPI/Framework/ModHelpers/ModHelper.cs index caa66bad..1cdd8536 100644 --- a/src/SMAPI/Framework/ModHelpers/ModHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModHelper.cs @@ -41,7 +41,7 @@ namespace StardewModdingAPI.Framework.ModHelpers source: this.Mod, nounPhrase: $"{nameof(IModHelper)}.{nameof(IModHelper.Content)}", version: "3.14.0", - severity: DeprecationLevel.Notice + severity: DeprecationLevel.Info ); return this.ContentImpl; diff --git a/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs b/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs index b3378ad1..5a850255 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs @@ -4,18 +4,31 @@ using Mono.Cecil; namespace StardewModdingAPI.Framework.ModLoading { /// A minimal assembly definition resolver which resolves references to known assemblies. - internal class AssemblyDefinitionResolver : DefaultAssemblyResolver + internal class AssemblyDefinitionResolver : IAssemblyResolver { /********* ** Fields *********/ + /// The underlying assembly resolver. + private readonly DefaultAssemblyResolverWrapper Resolver = new(); + /// The known assemblies. private readonly IDictionary Lookup = new Dictionary(); + /// The directory paths to search for assemblies. + private readonly HashSet SearchPaths = new(); + /********* ** Public methods *********/ + /// Construct an instance. + public AssemblyDefinitionResolver() + { + foreach (string path in this.Resolver.GetSearchDirectories()) + this.SearchPaths.Add(path); + } + /// Add known assemblies to the resolver. /// The known assemblies. public void Add(params AssemblyDefinition[] assemblies) @@ -29,7 +42,7 @@ namespace StardewModdingAPI.Framework.ModLoading /// The assembly names for which it should be returned. public void AddWithExplicitNames(AssemblyDefinition assembly, params string[] names) { - this.RegisterAssembly(assembly); + this.Resolver.AddAssembly(assembly); foreach (string name in names) this.Lookup[name] = assembly; } @@ -37,18 +50,52 @@ namespace StardewModdingAPI.Framework.ModLoading /// Resolve an assembly reference. /// The assembly name. /// The assembly can't be resolved. - 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); } /// Resolve an assembly reference. /// The assembly name. /// The assembly reader parameters. /// The assembly can't be resolved. - 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); + } + + /// Add a directory path to search for assemblies, if it's non-null and not already added. + /// The path to search. + /// Returns whether the path was successfully added. + public bool TryAddSearchDirectory(string? path) + { + if (path is not null && this.SearchPaths.Add(path)) + { + this.Resolver.AddSearchDirectory(path); + return true; + } + + return false; + } + + /// Remove a directory path to search for assemblies, if it's non-null. + /// The path to remove. + /// Returns whether the path was in the list and removed. + public bool RemoveSearchDirectory(string? path) + { + if (path is not null && this.SearchPaths.Remove(path)) + { + this.Resolver.RemoveSearchDirectory(path); + return true; + } + + return false; + } + + /// + public void Dispose() + { + this.Resolver.Dispose(); } @@ -63,5 +110,16 @@ namespace StardewModdingAPI.Framework.ModLoading ? match : null; } + + /// An internal wrapper around to allow access to its protected methods. + private class DefaultAssemblyResolverWrapper : DefaultAssemblyResolver + { + /// Add an assembly to the resolver. + /// The assembly to add. + public void AddAssembly(AssemblyDefinition assembly) + { + this.RegisterAssembly(assembly); + } + } } } diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index eb940c41..01037870 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -264,8 +264,15 @@ namespace StardewModdingAPI.Framework.ModLoading if (!file.Exists) 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 AssemblyDefinition assembly; + try { byte[] assemblyBytes = File.ReadAllBytes(file.FullName); 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 })); } } + finally + { + // clean up temporary search directory + if (temporarySearchDir is not null) + this.AssemblyDefinitionResolver.RemoveSearchDirectory(temporarySearchDir); + } // skip if already visited if (visitedAssemblyNames.Contains(assembly.Name.Name)) diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index abc46d47..c5648c74 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -47,7 +47,7 @@ namespace StardewModdingAPI.Framework.ModLoading IModMetadata 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 + else if (status == ModMetadataStatus.Failed) metadata.SetStatus(status, ModFailReason.InvalidManifest, folder.ManifestParseErrorText); yield return metadata; @@ -223,8 +223,8 @@ namespace StardewModdingAPI.Framework.ModLoading { foreach (IModMetadata mod in group) { - if (mod.Status == ModMetadataStatus.Failed) - continue; // don't replace metadata error + if (mod.Status == ModMetadataStatus.Failed && mod.FailReason != ModFailReason.InvalidManifest) + continue; 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}."); diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 46d65f6a..0f86ed6b 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; @@ -50,6 +49,7 @@ using StardewModdingAPI.Utilities; using StardewValley; using StardewValley.Menus; using StardewValley.Objects; +using StardewValley.SDKs; using xTile.Display; using LanguageCode = StardewValley.LocalizedContentManager.LanguageCode; using MiniMonoModHotfix = MonoMod.Utils.MiniMonoModHotfix; @@ -67,8 +67,11 @@ namespace StardewModdingAPI.Framework /**** ** Low-level components ****/ + /// A state which indicates whether SMAPI should exit immediately and any pending initialization should be cancelled. + private ExitState ExitState; + /// Whether the game should exit immediately and any pending initialization should be cancelled. - private bool IsExiting; + private bool IsExiting => this.ExitState != ExitState.None; /// Manages the SMAPI console window and log file. private readonly LogManager LogManager; @@ -297,22 +300,13 @@ namespace StardewModdingAPI.Framework this.IsGameRunning = true; StardewValley.Program.releaseBuild = true; // game's debug logic interferes with SMAPI opening the game window this.Game.Run(); + this.Dispose(isError: false); } catch (Exception ex) { this.LogManager.LogFatalLaunchError(ex); this.LogManager.PressAnyKeyToExit(); - } - 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); - } + this.Dispose(isError: true); } } @@ -327,6 +321,14 @@ namespace StardewModdingAPI.Framework /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. [SuppressMessage("ReSharper", "ConditionalAccessQualifierIsNonNullableAccordingToAPIContract", Justification = "May be disposed before SMAPI is fully initialized.")] public void Dispose() + { + this.Dispose(isError: true); // if we got here, SMAPI didn't detect the exit before it happened + } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// Whether the process is exiting due to an error or crash. + [SuppressMessage("ReSharper", "ConditionalAccessQualifierIsNonNullableAccordingToAPIContract", Justification = "May be disposed before SMAPI is fully initialized.")] + public void Dispose(bool isError) { // skip if already disposed if (this.IsDisposed) @@ -349,13 +351,29 @@ namespace StardewModdingAPI.Framework // dispose core components this.IsGameRunning = false; - this.IsExiting = true; + if (this.ExitState == ExitState.None || isError) + this.ExitState = isError ? ExitState.Crash : ExitState.GameExit; this.ContentCore?.Dispose(); this.Game?.Dispose(); 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) - Process.GetCurrentProcess().Kill(); + // clean up SDK + // 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,7 +405,14 @@ namespace StardewModdingAPI.Framework { string[] looseFiles = new DirectoryInfo(this.ModsPath).GetFiles().Select(p => p.Name).ToArray(); 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))}"); + } } // load manifests @@ -1250,7 +1275,7 @@ namespace StardewModdingAPI.Framework private void OnGameExiting() { this.Multiplayer.Disconnect(StardewValley.Multiplayer.DisconnectType.ClosedGame); - this.Dispose(); + this.Dispose(isError: false); } /// Raised when a mod network message is received. @@ -1670,7 +1695,7 @@ namespace StardewModdingAPI.Framework source: metadata, nounPhrase: $"{nameof(IAssetEditor)}", version: "3.14.0", - severity: DeprecationLevel.Notice, + severity: DeprecationLevel.Info, logStackTrace: false ); @@ -1683,7 +1708,7 @@ namespace StardewModdingAPI.Framework source: metadata, nounPhrase: $"{nameof(IAssetLoader)}", version: "3.14.0", - severity: DeprecationLevel.Notice, + severity: DeprecationLevel.Info, logStackTrace: false ); @@ -1715,7 +1740,7 @@ namespace StardewModdingAPI.Framework metadata, $"using {name} without bundling it", "3.14.7", - DeprecationLevel.Notice, + DeprecationLevel.Info, logStackTrace: false ); } @@ -2239,7 +2264,7 @@ namespace StardewModdingAPI.Framework this.Monitor.LogFatal(message); this.LogManager.WriteCrashLog(); - this.IsExiting = true; + this.ExitState = ExitState.Crash; this.Game.Exit(); } diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj index c05512e9..36db0545 100644 --- a/src/SMAPI/SMAPI.csproj +++ b/src/SMAPI/SMAPI.csproj @@ -16,6 +16,7 @@ false + app.manifest @@ -26,7 +27,7 @@ - + diff --git a/src/SMAPI/Utilities/PerScreen.cs b/src/SMAPI/Utilities/PerScreen.cs index 54657ade..468df0bd 100644 --- a/src/SMAPI/Utilities/PerScreen.cs +++ b/src/SMAPI/Utilities/PerScreen.cs @@ -59,7 +59,7 @@ namespace StardewModdingAPI.Utilities null, $"calling the {nameof(PerScreen)} constructor with null", "3.14.0", - DeprecationLevel.Notice + DeprecationLevel.Info ); #else throw new ArgumentNullException(nameof(createNewState)); diff --git a/src/SMAPI/app.manifest b/src/SMAPI/app.manifest new file mode 100644 index 00000000..42faff59 --- /dev/null +++ b/src/SMAPI/app.manifest @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true/pm + permonitorv2,permonitor + + + diff --git a/src/SMAPI/i18n/tr.json b/src/SMAPI/i18n/tr.json index e97a48ba..ba0c33c9 100644 --- a/src/SMAPI/i18n/tr.json +++ b/src/SMAPI/i18n/tr.json @@ -2,5 +2,5 @@ // short date format for SDate // tokens: {{day}} (like 15), {{season}} (like Spring), {{seasonLowercase}} (like spring), {{year}} (like 2) "generic.date": "{{day}} {{season}}", - "generic.date-with-year": "{{day}} {{season}} года {{year}}" + "generic.date-with-year": "{{day}} {{season}} Yıl {{year}}" }