Merge branch 'develop' into stable

This commit is contained in:
Jesse Plamondon-Willard 2022-08-20 17:01:59 -04:00
commit a1bc96d365
No known key found for this signature in database
GPG Key ID: CF8B1456B3E29F49
44 changed files with 359 additions and 123 deletions

3
.gitignore vendored
View File

@ -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

View File

@ -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-->

View File

@ -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.

View File

@ -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.

View File

@ -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

View File

@ -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;

View File

@ -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 "$@"
;;
*)

View File

@ -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) -->

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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);

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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
}
}

View File

@ -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" />

View File

@ -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());
}

View File

@ -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>

View File

@ -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)

View File

@ -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;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 831 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1 @@
<svg width="63" height="126" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#a)"><path d="M63.002 30.06c-18.36 0-33.297 14.778-33.297 32.94 0 18.163 14.938 32.94 33.297 32.94 18.359 0 33.295-14.776 33.295-32.94 0-18.162-14.936-32.94-33.295-32.94ZM115.603 63c0-1.674-.088-3.327-.244-4.96L126 53.78a61.838 61.838 0 0 0-2.182-9.52l-11.451.73a51.898 51.898 0 0 0-4.358-8.944l7.708-8.395a63.075 63.075 0 0 0-6.159-7.63L99.556 25.6a52.684 52.684 0 0 0-7.83-6.186l3.27-10.892a63.833 63.833 0 0 0-8.886-4.243l-6.57 9.323a52.763 52.763 0 0 0-9.774-2.198L67.934.187a65.106 65.106 0 0 0-9.87 0l-1.83 11.216A52.795 52.795 0 0 0 46.46 13.6l-6.568-9.323a63.782 63.782 0 0 0-8.886 4.243l3.271 10.892a52.659 52.659 0 0 0-7.832 6.186l-10.001-5.576a63.235 63.235 0 0 0-6.16 7.629l7.71 8.395a51.7 51.7 0 0 0-4.359 8.945l-11.452-.73A62.05 62.05 0 0 0 0 53.78l10.642 4.258a51.832 51.832 0 0 0-.245 4.96c0 1.673.088 3.326.245 4.96L0 72.217a62.178 62.178 0 0 0 2.182 9.52l11.452-.731a51.65 51.65 0 0 0 4.36 8.944l-7.711 8.396a63.311 63.311 0 0 0 6.16 7.629l10-5.576a52.731 52.731 0 0 0 7.833 6.186l-3.271 10.892a63.789 63.789 0 0 0 8.886 4.242l6.568-9.321a52.772 52.772 0 0 0 9.775 2.198l1.83 11.216c3.285.248 6.585.248 9.87 0l1.832-11.216a52.76 52.76 0 0 0 9.774-2.198l6.569 9.321a63.767 63.767 0 0 0 8.886-4.242l-3.269-10.892a52.904 52.904 0 0 0 7.83-6.186l10.002 5.576a63.1 63.1 0 0 0 6.159-7.63l-7.711-8.395a51.625 51.625 0 0 0 4.361-8.944l11.451.73a61.714 61.714 0 0 0 2.182-9.52l-10.641-4.258c.156-1.633.244-3.286.244-4.96V63Zm-52.601 38.253c-21.322 0-38.67-17.16-38.67-38.252 0-21.093 17.348-38.253 38.67-38.253 21.321 0 38.668 17.16 38.668 38.253s-17.347 38.252-38.668 38.252Z" fill="#C4C4C4"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h63v126H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -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");
}
);

View File

@ -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": {

View File

@ -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

View File

@ -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)}",

View File

@ -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;

View File

@ -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)}";

View File

@ -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
}
}

View File

@ -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);

View File

@ -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;
}

View File

@ -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;

View File

@ -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);
}
}
}
}

View File

@ -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))

View File

@ -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}.");

View File

@ -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);
}
}
@ -327,6 +321,14 @@ namespace StardewModdingAPI.Framework
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
[SuppressMessage("ReSharper", "ConditionalAccessQualifierIsNonNullableAccordingToAPIContract", Justification = "May be disposed before SMAPI is fully initialized.")]
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)
@ -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();
}

View File

@ -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" />

View File

@ -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));

41
src/SMAPI/app.manifest Normal file
View File

@ -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>

View File

@ -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}}"
}