diff options
author | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2022-08-20 17:01:59 -0400 |
---|---|---|
committer | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2022-08-20 17:01:59 -0400 |
commit | a1bc96d365dc40275f198668d3f4c09bd7a92613 (patch) | |
tree | 5a130399bf8031aa70defb71a121b740d7c6cd7a | |
parent | d51ffe58f7b7450cd4c4a7ee3d8b4da1cf55e7e4 (diff) | |
parent | f3a79219e85c9af18f2f6d8b2aaa794afa08578d (diff) | |
download | SMAPI-a1bc96d365dc40275f198668d3f4c09bd7a92613.tar.gz SMAPI-a1bc96d365dc40275f198668d3f4c09bd7a92613.tar.bz2 SMAPI-a1bc96d365dc40275f198668d3f4c09bd7a92613.zip |
Merge branch 'develop' into stable
44 files changed, 359 insertions, 123 deletions
@@ -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. <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <PropertyGroup> <!--set general build properties --> - <Version>3.15.1</Version> + <Version>3.16.0</Version> <Product>SMAPI</Product> <LangVersion>latest</LangVersion> <AssemblySearchPaths>$(AssemblySearchPaths);{GAC}</AssemblySearchPaths> <DefineConstants>$(DefineConstants);SMAPI_DEPRECATED</DefineConstants> - <DebugType>pdbonly</DebugType> <DebugSymbols>true</DebugSymbols> <!--enable nullable annotations, except in .NET Standard 2.0 where they aren't supported--> 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 @@ <!-- ## 4.0.0 * 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 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`<br />`--developer-mode-off` | Enable or disable features intended for mod developers. Currently this only makes `TRACE`-level messages appear in the console. -`--no-terminal` | The SMAPI launcher won't try to open a terminal window, and SMAPI won't log anything to the console. (Messages will still be written to the log file.) -`--use-current-shell` | The SMAPI launcher won't try to open a terminal window, but SMAPI will still log to the console. (Messages will still be written to the log file.) +`--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 } /// <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="options">The allowed options (not case sensitive).</param> /// <param name="indent">The indentation to prefix to output.</param> - private string InteractivelyChoose(string message, string[] options, string indent = "", Action<string>? print = null) + private string InteractivelyChoose(string message, string[] options, string indent = "", Action<string>? 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 **********************************************--> <PropertyGroup> - <!-- include PDB file by default to enable line numbers in stack traces --> - <DebugType>pdbonly</DebugType> + <!-- enable line numbers in stack traces --> <DebugSymbols>true</DebugSymbols> <!-- don't create the 'refs' folder (which isn't useful for mods) --> 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<ActionResult> 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); /// <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> 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> - 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> 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 - 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<string, List<LogModInfo>> mods = new Dictionary<string, List<LogModInfo>>(); bool inModList = false; bool inContentPackList = false; @@ -133,7 +133,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing if (!mods.TryGetValue(name, out List<LogModInfo>? entries)) 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; } @@ -156,7 +156,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing if (!mods.TryGetValue(name, out List<LogModInfo>? entries)) 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; } 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; + /// <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> - public bool IsMod { get; } + public bool IsMod => this.ModType != ModType.Special; /// <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> - [MemberNotNullWhen(true, nameof(LogModInfo.ContentPackFor))] - public bool IsContentPack { get; } + public bool IsContentPack => this.ModType == ModType.ContentPack; /********* ** Public methods *********/ /// <summary>Construct an instance.</summary> + /// <param name="modType">The mod type.</param> /// <param name="name">The mod name.</param> /// <param name="author">The mod author.</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="errors">The number of errors logged by this mod.</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(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 +{ + /// <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 + } +} 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 @@ </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.MemoryStorage" Version="1.7.0" /> <PackageReference Include="HtmlAgilityPack" Version="1.11.43" /> 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 { <link rel="stylesheet" href="~/Content/css/index.css" /> <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> SMAPI - <img id="pufferchick" src="Content/images/pufferchick.png" /> + <img id="pufferchick" src="Content/images/pufferchick.svg" /> </h1> <div id="blurb"> <p>The mod loader for Stardew Valley.</p> 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>(); 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<string, bool> defaultFilters = Enum @@ -242,7 +243,7 @@ else if (log?.IsValid == true) @if (log?.IsValid == true) { <div id="output"> - @if (outdatedMods.Any() || errorHandler is null || hasOlderErrorHandler) + @if (outdatedMods.Any() || errorHandler is null || hasOlderErrorHandler || isPyTkCompatibilityMode) { <h2>Suggested fixes</h2> <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> } + @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()) { <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> } </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)'] }"> <td><input type="checkbox" v-bind:checked="showMods['@Model.GetSlug(mod.Name)']" v-bind:class="{ invisible: !anyModsHidden }" /></td> <td> <strong v-pre>@mod.Name</strong> @mod.Version - @if (contentPackList != null) + @if (contentPackList.Any()) { <div v-if="!hideContentPacks" class="content-packs"> @foreach (var contentPack in contentPackList) @@ -369,7 +389,7 @@ else if (log?.IsValid == true) </td> <td> @mod.Author - @if (contentPackList != null) + @if (contentPackList.Any()) { <div v-if="!hideContentPacks" class="content-packs"> @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 Binary files differdeleted file mode 100644 index f359146c..00000000 --- a/src/SMAPI.Web/wwwroot/Content/images/pufferchick-cool.png +++ /dev/null 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 @@ +<svg width="56" height="56" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M0 17.684h2.947v8.842H0v-8.842Zm29.474 32.421h2.947V56h-2.947v-5.895ZM18.667 29.474h2.947v5.894h-2.947v-5.894Zm16.701 20.631h2.948V56h-2.948v-5.895ZM2.948 14.737h5.894v2.947H2.947v-2.947Zm5.894 2.947h5.895v2.948H8.842v-2.948Zm0 23.58h5.895v2.947H8.842v-2.948ZM23.58 5.893h5.895v2.948H23.58V5.895Z" fill="#6B3407"/><path d="M14.737 41.263h5.895v2.948h-5.895v-2.948Zm29.474 0h5.894v2.948h-5.894v-2.948ZM32.42 23.58h5.895v2.947H32.42V23.58Z" fill="#8F4039"/><path d="M26.526 2.947h5.895v2.948h-5.895V2.947Z" fill="#FF4D1E"/><path d="M32.421 8.842h5.895v2.947H32.42V8.843Z" fill="#8F4039"/><path d="M23.579 47.158h14.737v2.947H23.579v-2.947Zm-1.965-11.79h11.79v2.948h-11.79v-2.948ZM20.632 0h11.79v2.947h-11.79V0Zm0 2.947h2.947v2.948h-2.947V2.947Zm11.79 0h2.946v2.948h-2.947V2.947Zm2.946 2.948h2.948v2.947h-2.948V5.895ZM2.948 26.526h2.947v2.948H2.947v-2.948Z" fill="#6B3407"/><path d="M2.947 23.579h2.948v2.947H2.947V23.58Zm2.948 2.947h2.947v2.948H5.895v-2.948Zm-2.948 5.895h2.948v2.947H2.947v-2.947Zm5.895 5.895h2.947v2.947H8.843v-2.947Zm9.825-2.948h2.947v2.948h-2.947v-2.948Zm14.736-2.947h2.948v2.947h-2.947v-2.947Z" fill="#9A4E0F"/><path d="M5.895 38.316h2.947v2.947H5.895v-2.947Zm15.719-8.842h2.947v2.947h-2.947v-2.947Z" fill="#6B3407"/><path d="M17.684 44.21h2.948v2.948h-2.948V44.21Zm5.895 0h2.947v2.948H23.58V44.21Zm20.632 0h2.947v2.948H44.21V44.21Zm5.894-5.894h2.948v2.947h-2.948v-2.947Zm-2.947-2.948h2.947v2.948h-2.947v-2.948Zm5.895-8.842H56v2.948h-2.947v-2.948Zm-2.948-2.947h2.948v2.947h-2.948V23.58ZM26.526 8.842h2.948v2.947h-2.948V8.843Z" fill="#8F4039"/><path d="M23.579 2.947h2.947v2.948H23.58V2.947Zm8.842 2.948h2.947v2.947h-2.947V5.895Z" fill="#C31E1E"/><path d="M29.474 5.895h2.947v2.947h-2.947V5.895Z" fill="#FF4D1E"/><path d="M23.579 11.79h2.947v2.947H23.58V11.79Z" fill="#8F4039"/><path d="M2.947 17.684h2.948v2.948H2.947v-2.948Z" fill="#FFE2AF"/><path d="M5.895 17.684h2.947v2.948H5.895v-2.948Zm-2.948 2.948h2.948v2.947H2.947v-2.947Zm8.843 0h2.947v2.947H11.79v-2.947Zm12.771 8.842h2.948v2.947H24.56v-2.947Zm8.843 0h2.947v2.947h-2.947v-2.947Zm0 5.894h2.947v2.948h-2.947v-2.948ZM8.841 26.526h2.947v5.895H8.843v-5.895Zm-5.895 2.948h2.948v2.947H2.947v-2.947Zm11.79 5.894h2.947v2.948h-2.947v-2.948Z" fill="#DEA159"/><path d="M14.737 38.316h2.947v2.947h-2.947v-2.947Zm5.895 2.947h2.947v2.948h-2.947v-2.948Zm23.579-8.842h2.947v2.947H44.21v-2.947Zm0-5.895h2.947v2.948H44.21v-2.948Zm-17.685 0h14.737v2.948H26.526v-2.948Zm0 11.79h5.895v2.947h-5.895v-2.947Zm5.895 2.947h8.842v2.948h-8.842v-2.948Zm2.947-2.947h8.843v2.947h-8.843v-2.947Zm2.948-2.948h8.842v2.948h-8.842v-2.948Zm0-11.79h8.842v2.948h-8.842V23.58Zm-2.948-8.841h8.843v2.947h-8.843v-2.947Zm-11.79 0h8.843v2.947H23.58v-2.947Zm0 8.842h8.843v2.947H23.58V23.58Zm5.896-5.895h11.79v5.895h-11.79v-5.895Zm6.877 14.737h4.912v2.947h-4.912v-2.947Zm0-2.947h13.754v2.947H36.351v-2.947ZM26.526 11.79h8.842v2.947h-8.842V11.79Z" fill="#FCD100"/><path d="M20.632 44.21h2.947v2.948h-2.947V44.21Z" fill="#DF8900"/><path d="M20.632 44.21h2.947v2.948h-2.947V44.21Zm20.631-2.947h2.948v2.948h-2.948v-2.948Zm2.948-2.947h2.947v2.947H44.21v-2.947Zm2.947-5.895h2.947v2.947h-2.947v-2.947Zm-5.895 0h2.948v2.947h-2.948v-2.947Zm2.948-14.737h2.947v2.948H44.21v-2.948Zm-8.843-5.895h2.948v2.948h-2.948V11.79Zm-14.736 2.948h2.947v2.947h-2.947v-2.947Zm-2.948 2.947h2.948v2.948h-2.948v-2.948Zm5.895 8.842h2.947v2.948H23.58v-2.948Zm8.842 11.79h2.947v2.947h-2.947v-2.947Zm14.737-14.737h2.947v5.895h-2.947v-5.895ZM18.667 38.316h7.86v2.947h-7.86v-2.947Zm4.912 2.947h8.842v2.948H23.58v-2.948Zm2.947 2.948h11.79v2.947h-11.79V44.21Zm-8.842-14.737h.983v11.79h-.983v-11.79Zm-2.947 2.947h2.947v2.947h-2.947v-2.947Zm0-8.842h2.947v2.947h-2.947V23.58Zm0 2.947h2.947v2.948h-2.947v-2.948Zm2.947 0h2.948v2.948h-2.948v-2.948Z" fill="#DF8900"/><path d="M41.263 44.21h2.948v2.948h-2.948V44.21Zm5.895-5.894h2.947v2.947h-2.947v-2.947Zm-32.421-8.842h2.947v2.947h-2.947v-2.947Zm5.895-5.895h2.947v5.895h-2.947v-5.895Zm0-11.79h2.947v2.948h-2.947V11.79Zm0 5.895h2.947v2.948h-2.947v-2.948Zm-2.948 2.948h2.948v5.894h-2.948v-5.894Zm32.421 5.894h2.948v2.948h-2.948v-2.948Zm-5.894-5.894h2.947v2.947H44.21v-2.947Zm-20.632-2.948h2.947v5.895H23.58v-5.895Z" fill="#FCD100"/><path d="M8.842 35.368h2.947v2.948H8.843v-2.948Zm-2.947-2.947h2.947v5.895H5.895V32.42Zm5.895-2.947h2.947v11.79H11.79v-11.79Z" fill="#DEA159"/><path d="M5.895 29.474h2.947v2.947H5.895v-2.947Zm2.947 2.947h2.947v2.947H8.843v-2.947Zm12.772 0h11.79v2.947h-11.79v-2.947Zm5.895-2.947h5.895v2.947h-5.895v-2.947ZM5.895 20.632h5.895v5.894H5.895v-5.894Zm5.895 2.947h2.947v5.895H11.79v-5.895Z" fill="#FFE2AF"/><path d="M20.632 8.842h2.947v2.947h-2.947V8.843Zm17.684 2.947h2.947v2.948h-2.947V11.79Z" fill="#8F4039"/><path d="M41.263 17.684h2.948v2.948h-2.948v-2.948Z" fill="#2F2000"/><path d="M41.263 20.632h2.948v2.947h-2.948v-2.947Z" fill="#F4F7AC"/><path d="M26.526 17.684h2.948v2.948h-2.948v-2.948Z" fill="#2F2000"/><path d="M26.526 20.632h2.948v2.947h-2.948v-2.947Z" fill="#F4F7AC"/><path d="M20.632 20.632h2.947v2.947h-2.947v-2.947Zm8.842-11.79h2.947v2.947h-2.947V8.843Zm2.947 5.895h2.947v2.947h-2.947v-2.947Zm8.842-2.947h2.948v2.947h-2.948V11.79Zm0 14.736h2.948v2.948h-2.948v-2.948Zm-4.912 8.842h1.965v2.948H36.35v-2.948Z" fill="#FFFF0C"/><path d="M41.263 8.842h2.948v2.947h-2.948V8.843Zm8.842 20.632h2.948v5.894h-2.948v-5.894Zm-2.947-11.79h2.947v5.895h-2.947v-5.895ZM44.21 11.79h2.947v5.895H44.21V11.79Zm-26.527 0h2.948v5.895h-2.948V11.79Zm-2.947 5.895h2.947v5.895h-2.947v-5.895Zm23.579 26.527h2.947v2.947h-2.947V44.21Zm2.947 2.947h2.948v2.947h-2.948v-2.947Z" fill="#8F4039"/><path d="M2.947 35.368h2.948v2.948H2.947v-2.948ZM0 29.474h2.947v5.894H0v-5.894Z" fill="#6B3407"/><path d="M14.832 16.702v1.52h2.25v1.522h1.522v1.521h1.52v1.522h1.521v1.52h7.163v-1.52h1.521v-1.522h1.521v-1.52h5.008v1.52h1.522v1.522h1.555v1.519h6.24v-1.519h1.87v-1.522h1.522v-1.52h1.52v-3.043H14.833Z" fill="#000"/><path d="M19.65 18.223v1.521h1.52v-1.521h-1.52Zm1.52 1.521v1.521h1.522v-1.52H21.17Zm1.522 0h1.52v-1.521h-1.521v1.521Zm1.52 0v1.521h1.521v-1.52h-1.52Zm13.692-1.521h1.521v1.521h1.52v-1.521h1.523v1.521h1.52v1.521h-1.52v-1.52h-1.522l-.001 1.52h-1.521l.001-1.52h-1.522l.001-1.522Z" fill="#436173"/><path d="M20.125 18.223v1.521h1.52v-1.521h-1.52Zm1.52 1.521v1.521h1.522v-1.52h-1.522Zm1.522 0h1.521v-1.521h-1.522l.001 1.521Zm1.521 0v1.521h1.52v-1.52h-1.52Zm13.692-1.521h1.52v1.521h1.521v-1.521h1.522v1.521h1.52v1.521h-1.52v-1.52h-1.522v1.52h-1.522l.001-1.52h-1.52v-1.522Z" fill="#667575"/></svg>
\ 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 Binary files differdeleted file mode 100644 index 1de9cf47..00000000 --- a/src/SMAPI.Web/wwwroot/Content/images/pufferchick.png +++ /dev/null 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 @@ +<svg width="56" height="56" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M0 17.684h2.947v8.842H0v-8.842Zm29.474 32.421h2.947V56h-2.947v-5.895ZM18.667 29.474h2.947v5.894h-2.947v-5.894Zm16.701 20.631h2.948V56h-2.948v-5.895ZM2.947 14.737h5.895v2.947H2.947v-2.947Zm5.895 2.947h5.895v2.948H8.842v-2.948Zm0 23.579h5.895v2.948H8.842v-2.948ZM23.579 5.895h5.895v2.947h-5.895V5.895Z" fill="#6B3407"/><path d="M14.737 41.263h5.895v2.948h-5.895v-2.948Zm29.473 0h5.895v2.948h-5.894v-2.948ZM32.421 23.579h5.895v2.947H32.42V23.58Z" fill="#8F4039"/><path d="M26.526 2.947h5.895v2.948h-5.895V2.947Z" fill="#FF4D1E"/><path d="M32.421 8.842h5.895v2.947H32.42V8.843Z" fill="#8F4039"/><path d="M23.579 47.158h14.737v2.947H23.579v-2.947Zm-1.965-11.79h11.79v2.948h-11.79v-2.948ZM20.632 0h11.79v2.947h-11.79V0Zm0 2.947h2.947v2.948h-2.947V2.947Zm11.789 0h2.947v2.948h-2.947V2.947Zm2.947 2.948h2.948v2.947h-2.948V5.895ZM2.947 26.526h2.948v2.948H2.947v-2.948Z" fill="#6B3407"/><path d="M2.947 23.579h2.948v2.947H2.947V23.58Zm2.948 2.947h2.947v2.948H5.895v-2.948Zm-2.948 5.895h2.948v2.947H2.947v-2.947Zm5.895 5.895h2.947v2.947H8.843v-2.947Zm9.825-2.948h2.947v2.948h-2.947v-2.948Zm14.737-2.947h2.947v2.947h-2.947v-2.947Z" fill="#9A4E0F"/><path d="M5.895 38.316h2.947v2.947H5.895v-2.947Zm15.719-8.842h2.947v2.947h-2.947v-2.947Z" fill="#6B3407"/><path d="M17.684 44.21h2.948v2.948h-2.948V44.21Zm5.895 0h2.947v2.948H23.58V44.21Zm20.631 0h2.948v2.948H44.21V44.21Zm5.895-5.894h2.948v2.947h-2.948v-2.947Zm-2.947-2.948h2.947v2.948h-2.947v-2.948Zm5.895-8.842H56v2.948h-2.947v-2.948Zm-2.948-2.947h2.948v2.947h-2.948V23.58ZM26.526 8.842h2.948v2.947h-2.948V8.843Z" fill="#8F4039"/><path d="M23.579 2.947h2.947v2.948H23.58V2.947Zm8.842 2.948h2.947v2.947h-2.947V5.895Z" fill="#C31E1E"/><path d="M29.474 5.895h2.947v2.947h-2.947V5.895Z" fill="#FF4D1E"/><path d="M23.579 11.79h2.947v2.947H23.58V11.79Z" fill="#8F4039"/><path d="M2.947 17.684h2.948v2.948H2.947v-2.948Z" fill="#FFE2AF"/><path d="M5.895 17.684h2.947v2.948H5.895v-2.948Zm-2.948 2.948h2.948v2.947H2.947v-2.947Zm8.843 0h2.947v2.947H11.79v-2.947Zm12.771 8.842h2.948v2.947H24.56v-2.947Zm8.843 0h2.947v2.947h-2.947v-2.947Zm0 5.894h2.947v2.948h-2.947v-2.948ZM8.842 26.526h2.947v5.895H8.843v-5.895Zm-5.895 2.948h2.948v2.947H2.947v-2.947Zm11.79 5.894h2.947v2.948h-2.947v-2.948Z" fill="#DEA159"/><path d="M14.737 38.316h2.947v2.947h-2.947v-2.947Zm5.895 2.947h2.947v2.948h-2.947v-2.948Zm23.578-8.842h2.948v2.947H44.21v-2.947Zm0-5.895h2.948v2.948H44.21v-2.948Zm-17.684 0h14.737v2.948H26.526v-2.948Zm0 11.79h5.895v2.947h-5.895v-2.947Zm5.895 2.947h8.842v2.948h-8.842v-2.948Z" fill="#FCD100"/><path d="M35.368 38.316h8.843v2.947h-8.843v-2.947Zm2.948-2.948h8.842v2.948h-8.842v-2.948Zm0-11.789h8.842v2.947h-8.842V23.58Zm-2.948-8.842h8.843v2.947h-8.843v-2.947Zm-11.789 0h8.842v2.947H23.58v-2.947Zm0 8.842h8.842v2.947H23.58V23.58Zm5.895-5.895h11.79v5.895h-11.79v-5.895Zm6.876 14.737h4.913v2.947h-4.912v-2.947Zm0-2.947h13.755v2.947H36.351v-2.947ZM26.526 11.79h8.842v2.947h-8.842V11.79Z" fill="#FCD100"/><path d="M20.632 44.21h2.947v2.948h-2.947V44.21Z" fill="#DF8900"/><path d="M20.632 44.21h2.947v2.948h-2.947V44.21Zm20.631-2.947h2.948v2.948h-2.948v-2.948Z" fill="#DF8900"/><path d="M44.21 38.316h2.948v2.947H44.21v-2.947Zm2.948-5.895h2.947v2.947h-2.947v-2.947Zm-5.895 0h2.948v2.947h-2.948v-2.947Zm2.947-14.737h2.948v2.948H44.21v-2.948Zm-8.842-5.894h2.948v2.947h-2.948V11.79Zm-14.736 2.947h2.947v2.947h-2.947v-2.947Zm-2.948 2.947h2.948v2.948h-2.948v-2.948Zm5.895 8.842h2.947v2.948H23.58v-2.948Zm8.842 11.79h2.947v2.947h-2.947v-2.947Zm14.737-14.737h2.947v5.895h-2.947v-5.895ZM18.667 38.316h7.86v2.947h-7.86v-2.947Z" fill="#DF8900"/><path d="M23.579 41.263h8.842v2.948H23.58v-2.948Z" fill="#DF8900"/><path d="M26.526 44.21h11.79v2.948h-11.79V44.21Zm-8.842-14.736h.983v11.79h-.983v-11.79Zm-2.947 2.947h2.947v2.947h-2.947v-2.947Zm0-8.842h2.947v2.947h-2.947V23.58Zm0 2.947h2.947v2.948h-2.947v-2.948Z" fill="#DF8900"/><path d="M17.684 26.526h2.948v2.948h-2.948v-2.948Z" fill="#DF8900"/><path d="M41.263 44.21h2.948v2.948h-2.948V44.21Zm5.895-5.894h2.947v2.947h-2.947v-2.947Zm-32.421-8.842h2.947v2.947h-2.947v-2.947Zm5.895-5.895h2.947v5.895h-2.947v-5.895Zm0-11.789h2.947v2.947h-2.947V11.79Zm0 5.894h2.947v2.948h-2.947v-2.948Zm-2.948 2.948h2.948v5.894h-2.948v-5.894Zm32.421 5.894h2.948v2.948h-2.948v-2.948Zm-5.895-5.894h2.948v2.947H44.21v-2.947Zm-20.631-2.948h2.947v5.895H23.58v-5.895Z" fill="#FCD100"/><path d="M8.842 35.368h2.947v2.948H8.843v-2.948Zm-2.947-2.947h2.947v5.895H5.895V32.42Zm5.895-2.947h2.947v11.79H11.79v-11.79Z" fill="#DEA159"/><path d="M5.895 29.474h2.947v2.947H5.895v-2.947Zm2.947 2.947h2.947v2.947H8.843v-2.947Zm12.772 0h11.79v2.947h-11.79v-2.947Zm5.895-2.947h5.895v2.947h-5.895v-2.947ZM5.895 20.632h5.895v5.894H5.895v-5.894Zm5.895 2.947h2.947v5.895H11.79v-5.895Z" fill="#FFE2AF"/><path d="M20.632 8.842h2.947v2.947h-2.947V8.843Zm17.684 2.948h2.947v2.947h-2.947V11.79Z" fill="#8F4039"/><path d="M41.263 17.684h2.948v2.948h-2.948v-2.948Z" fill="#2F2000"/><path d="M41.263 20.632h2.948v2.947h-2.948v-2.947Z" fill="#F4F7AC"/><path d="M26.526 17.684h2.948v2.948h-2.948v-2.948Z" fill="#2F2000"/><path d="M26.526 20.632h2.948v2.947h-2.948v-2.947Z" fill="#F4F7AC"/><path d="M20.632 20.632h2.947v2.947h-2.947v-2.947Zm8.842-11.79h2.947v2.947h-2.947V8.843Zm2.947 5.895h2.947v2.947h-2.947v-2.947Zm8.842-2.947h2.948v2.947h-2.948V11.79Zm0 14.736h2.948v2.948h-2.948v-2.948Zm-4.913 8.842h1.966v2.948H36.35v-2.948Z" fill="#FFFF0C"/><path d="M41.263 8.842h2.948v2.947h-2.948V8.843Zm8.842 20.632h2.948v5.894h-2.948v-5.894Zm-2.947-11.79h2.947v5.895h-2.947v-5.895ZM44.21 11.79h2.948v5.894H44.21V11.79Zm-26.526 0h2.948v5.894h-2.948V11.79Zm-2.947 5.894h2.947v5.895h-2.947v-5.895ZM38.316 44.21h2.947v2.948h-2.947V44.21Zm2.947 2.948h2.948v2.947h-2.948v-2.947Z" fill="#8F4039"/><path d="M2.947 35.368h2.948v2.948H2.947v-2.948ZM0 29.474h2.947v5.894H0v-5.894Z" fill="#6B3407"/></svg>
\ 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 Binary files differdeleted file mode 100644 index 48e9af5a..00000000 --- a/src/SMAPI.Web/wwwroot/Content/images/sidebar-bg.gif +++ /dev/null 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 @@ +<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>
\ 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; } /// <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> @@ -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 +{ + /// <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 + } +} 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 { /// <summary>A minimal assembly definition resolver which resolves references to known assemblies.</summary> - internal class AssemblyDefinitionResolver : DefaultAssemblyResolver + internal class AssemblyDefinitionResolver : IAssemblyResolver { /********* ** Fields *********/ + /// <summary>The underlying assembly resolver.</summary> + private readonly DefaultAssemblyResolverWrapper Resolver = new(); + /// <summary>The known assemblies.</summary> 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 *********/ + /// <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> /// <param name="assemblies">The known assemblies.</param> 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> 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 /// <summary>Resolve an assembly reference.</summary> /// <param name="name">The assembly name.</param> /// <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> /// <param name="name">The assembly name.</param> /// <param name="parameters">The assembly reader parameters.</param> /// <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 : 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); + } + } } } 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 ****/ + /// <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> - private bool IsExiting; + private bool IsExiting => this.ExitState != ExitState.None; /// <summary>Manages the SMAPI console window and log file.</summary> 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); } } @@ -328,6 +322,14 @@ namespace StardewModdingAPI.Framework [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 + } + + /// <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 if (this.IsDisposed) return; @@ -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); } /// <summary>Raised when a mod network message is received.</summary> @@ -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 @@ <!-- tiered compilation breaks Harmony --> <TieredCompilation>false</TieredCompilation> + <ApplicationManifest>app.manifest</ApplicationManifest> </PropertyGroup> <Import Project="..\..\build\common.targets" /> @@ -26,7 +27,7 @@ <PackageReference Include="MonoMod.Common" Version="22.3.5.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.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="System.Reflection.Emit" Version="4.7.0" /> 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<T>)} 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 @@ +<?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> 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}}" } |