Merge branch 'develop' into stable

This commit is contained in:
Jesse Plamondon-Willard 2022-05-15 19:14:40 -04:00
commit 5731b015a0
No known key found for this signature in database
GPG Key ID: CF8B1456B3E29F49
25 changed files with 121 additions and 61 deletions

View File

@ -1,7 +1,7 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<!--set general build properties -->
<Version>3.14.3</Version>
<Version>3.14.4</Version>
<Product>SMAPI</Product>
<LangVersion>latest</LangVersion>
<AssemblySearchPaths>$(AssemblySearchPaths);{GAC}</AssemblySearchPaths>

View File

@ -1,6 +1,16 @@
← [README](README.md)
# Release notes
## 3.14.4
Released 15 May 2022 for Stardew Valley 1.5.6 or later.
* For players:
* Improved performance for mods using deprecated APIs.
* For mod authors:
* Removed warning for mods which use `dynamic`.
_This no longer causes errors on Linux/macOS after Stardew Valley 1.5.5._
## 3.14.3
Released 12 May 2022 for Stardew Valley 1.5.6 or later.

View File

@ -1,9 +1,9 @@
{
"Name": "Console Commands",
"Author": "SMAPI",
"Version": "3.14.3",
"Version": "3.14.4",
"Description": "Adds SMAPI console commands that let you manipulate the game.",
"UniqueID": "SMAPI.ConsoleCommands",
"EntryDll": "ConsoleCommands.dll",
"MinimumApiVersion": "3.14.3"
"MinimumApiVersion": "3.14.4"
}

View File

@ -1,9 +1,9 @@
{
"Name": "Error Handler",
"Author": "SMAPI",
"Version": "3.14.3",
"Version": "3.14.4",
"Description": "Handles some common vanilla errors to log more useful info or avoid breaking the game.",
"UniqueID": "SMAPI.ErrorHandler",
"EntryDll": "ErrorHandler.dll",
"MinimumApiVersion": "3.14.3"
"MinimumApiVersion": "3.14.4"
}

View File

@ -1,9 +1,9 @@
{
"Name": "Save Backup",
"Author": "SMAPI",
"Version": "3.14.3",
"Version": "3.14.4",
"Description": "Automatically backs up all your saves once per day into its folder.",
"UniqueID": "SMAPI.SaveBackup",
"EntryDll": "SaveBackup.dll",
"MinimumApiVersion": "3.14.3"
"MinimumApiVersion": "3.14.4"
}

View File

@ -19,6 +19,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
PatchesGame = 4,
/// <summary>The mod uses the <c>dynamic</c> keyword which won't work on Linux/macOS.</summary>
[Obsolete("This value is no longer used by SMAPI and will be removed in the upcoming SMAPI 4.0.0.")]
UsesDynamic = 8,
/// <summary>The mod references specialized 'unvalidated update tick' events which may impact stability.</summary>

View File

@ -57,7 +57,7 @@ namespace StardewModdingAPI.Toolkit
/// <summary>Extract mod metadata from the wiki compatibility list.</summary>
public async Task<WikiModList> GetWikiCompatibilityListAsync()
{
WikiClient client = new(this.UserAgent);
using WikiClient client = new(this.UserAgent);
return await client.FetchModsAsync();
}

View File

@ -50,7 +50,7 @@ namespace StardewModdingAPI
internal static int? LogScreenId { get; set; }
/// <summary>SMAPI's current raw semantic version.</summary>
internal static string RawApiVersion = "3.14.3";
internal static string RawApiVersion = "3.14.4";
}
/// <summary>Contains SMAPI's constants and assumptions.</summary>
@ -84,7 +84,7 @@ namespace StardewModdingAPI
get
{
SCore.DeprecationManager.Warn(
source: SCore.DeprecationManager.GetModFromStack(),
source: null,
nounPhrase: $"{nameof(Constants)}.{nameof(Constants.ExecutionPath)}",
version: "3.14.0",
severity: DeprecationLevel.Notice

View File

@ -1,6 +1,7 @@
using System.Collections.Generic;
using StardewModdingAPI.Enums;
using StardewModdingAPI.Events;
using StardewModdingAPI.Framework;
using StardewModdingAPI.Utilities;
using StardewValley;
using StardewValley.Menus;
@ -41,6 +42,10 @@ namespace StardewModdingAPI
/// <summary>Whether the in-game world is completely unloaded and not in the process of being loaded. The world may still exist in memory at this point, but should be ignored.</summary>
internal static bool IsWorldFullyUnloaded => Context.LoadStage is LoadStage.ReturningToTitle or LoadStage.None;
/// <summary>If SMAPI is currently waiting for mod code, the mods to which it belongs (with the most recent at the top of the stack).</summary>
/// <remarks><strong>This is heuristic only.</strong> It provides a quick way to identify the most likely mod for deprecation warnings, but it should be followed with a more accurate check if needed.</remarks>
internal static Stack<IModMetadata> HeuristicModsRunningCode { get; } = new();
/*********
** Accessors

View File

@ -36,7 +36,7 @@ namespace StardewModdingAPI.Framework.Content
get
{
SCore.DeprecationManager.Warn(
source: SCore.DeprecationManager.GetModFromStack(),
source: null,
nounPhrase: $"{nameof(IAssetInfo)}.{nameof(IAssetInfo.AssetName)}",
version: "3.14.0",
severity: DeprecationLevel.Notice,
@ -76,7 +76,7 @@ namespace StardewModdingAPI.Framework.Content
public bool AssetNameEquals(string path)
{
SCore.DeprecationManager.Warn(
source: SCore.DeprecationManager.GetModFromStack(),
source: null,
nounPhrase: $"{nameof(IAssetInfo)}.{nameof(IAssetInfo.AssetNameEquals)}",
version: "3.14.0",
severity: DeprecationLevel.Notice,

View File

@ -64,6 +64,7 @@ namespace StardewModdingAPI.Framework.Content
// check edit
if (this.Instance is IAssetEditor editor)
{
Context.HeuristicModsRunningCode.Push(this.Mod);
try
{
if (editor.CanEdit<TAsset>(asset))
@ -73,11 +74,16 @@ namespace StardewModdingAPI.Framework.Content
{
this.Mod.LogAsMod($"Mod failed when checking whether it could edit asset '{asset.Name}'. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
}
finally
{
Context.HeuristicModsRunningCode.TryPop(out _);
}
}
// check load
if (this.Instance is IAssetLoader loader)
{
Context.HeuristicModsRunningCode.Push(this.Mod);
try
{
if (loader.CanLoad<TAsset>(asset))
@ -87,6 +93,10 @@ namespace StardewModdingAPI.Framework.Content
{
this.Mod.LogAsMod($"Mod failed when checking whether it could load asset '{asset.Name}'. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
}
finally
{
Context.HeuristicModsRunningCode.TryPop(out _);
}
}
return false;

View File

@ -595,6 +595,7 @@ namespace StardewModdingAPI.Framework
foreach (ModLinked<IAssetLoader> loader in this.Loaders)
{
// check if loader applies
Context.HeuristicModsRunningCode.Push(loader.Mod);
try
{
if (!loader.Data.CanLoad<T>(legacyInfo))
@ -605,6 +606,10 @@ namespace StardewModdingAPI.Framework
loader.Mod.LogAsMod($"Mod failed when checking whether it could load asset '{legacyInfo.Name}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
continue;
}
finally
{
Context.HeuristicModsRunningCode.TryPop(out _);
}
// add operation
group ??= new AssetOperationGroup(new List<AssetLoadOperation>(), new List<AssetEditOperation>());
@ -617,9 +622,7 @@ namespace StardewModdingAPI.Framework
Mod: loader.Mod,
OnBehalfOf: null,
Priority: AssetLoadPriority.Exclusive,
GetData: assetInfo => loader.Data.Load<T>(
this.GetLegacyAssetInfo(assetInfo)
)
GetData: assetInfo => loader.Data.Load<T>(this.GetLegacyAssetInfo(assetInfo))
)
)
);
@ -629,6 +632,7 @@ namespace StardewModdingAPI.Framework
foreach (var editor in this.Editors)
{
// check if editor applies
Context.HeuristicModsRunningCode.Push(editor.Mod);
try
{
if (!editor.Data.CanEdit<T>(legacyInfo))
@ -639,6 +643,10 @@ namespace StardewModdingAPI.Framework
editor.Mod.LogAsMod($"Mod crashed when checking whether it could edit asset '{legacyInfo.Name}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
continue;
}
finally
{
Context.HeuristicModsRunningCode.TryPop(out _);
}
// HACK
//
@ -672,9 +680,7 @@ namespace StardewModdingAPI.Framework
Mod: editor.Mod,
OnBehalfOf: null,
Priority: priority,
ApplyEdit: assetData => editor.Data.Edit<T>(
this.GetLegacyAssetData(assetData)
)
ApplyEdit: assetData => editor.Data.Edit<T>(this.GetLegacyAssetData(assetData))
)
)
);

View File

@ -177,6 +177,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
// fetch asset from loader
IModMetadata mod = loader.Mod;
T data;
Context.HeuristicModsRunningCode.Push(loader.Mod);
try
{
data = (T)loader.GetData(info);
@ -187,6 +188,10 @@ namespace StardewModdingAPI.Framework.ContentManagers
mod.LogAsMod($"Mod crashed when loading asset '{info.Name}'{this.GetOnBehalfOfLabel(loader.OnBehalfOf)}. SMAPI will use the default asset instead. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
return null;
}
finally
{
Context.HeuristicModsRunningCode.TryPop(out _);
}
// return matched asset
return this.TryFixAndValidateLoadedAsset(info, data, loader)
@ -229,6 +234,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
// try edit
object prevAsset = asset.Data;
Context.HeuristicModsRunningCode.Push(editor.Mod);
try
{
editor.ApplyEdit(asset);
@ -238,9 +244,13 @@ namespace StardewModdingAPI.Framework.ContentManagers
{
mod.LogAsMod($"Mod crashed when editing asset '{info.Name}'{this.GetOnBehalfOfLabel(editor.OnBehalfOf)}, which may cause errors in-game. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
}
finally
{
Context.HeuristicModsRunningCode.TryPop(out _);
}
// validate edit
// ReSharper disable once ConditionIsAlwaysTrueOrFalse -- it's only guaranteed non-null after this method
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract -- it's only guaranteed non-null after this method
if (asset.Data == null)
{
mod.LogAsMod($"Mod incorrectly set asset '{info.Name}'{this.GetOnBehalfOfLabel(editor.OnBehalfOf)} to a null value; ignoring override.", LogLevel.Warn);

View File

@ -121,7 +121,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
}
catch (Exception ex) when (ex is not SContentLoadException)
{
throw this.GetLoadError(assetName, ContentLoadErrorType.Other, "an unexpected occurred.", ex);
throw this.GetLoadError(assetName, ContentLoadErrorType.Other, "an unexpected error occurred.", ex);
}
// track & return asset

View File

@ -37,13 +37,6 @@ namespace StardewModdingAPI.Framework.Deprecations
this.ModRegistry = modRegistry;
}
/// <summary>Get a mod for the closest assembly registered as a source of deprecation warnings.</summary>
/// <returns>Returns the source name, or <c>null</c> if no registered assemblies were found.</returns>
public IModMetadata? GetModFromStack()
{
return this.ModRegistry.GetFromStack();
}
/// <summary>Get a mod from its unique ID.</summary>
/// <param name="modId">The mod's unique ID.</param>
public IModMetadata? GetMod(string modId)
@ -52,7 +45,7 @@ namespace StardewModdingAPI.Framework.Deprecations
}
/// <summary>Log a deprecation warning.</summary>
/// <param name="source">The mod which used the deprecated code, if known.</param>
/// <param name="source">The mod which used the deprecated code, or <c>null</c> to get it heuristically. Note that getting it heuristically is very slow in some cases, and should be avoided if at all possible.</param>
/// <param name="nounPhrase">A noun phrase describing what is deprecated.</param>
/// <param name="version">The SMAPI version which deprecated it.</param>
/// <param name="severity">How deprecated the code is.</param>
@ -60,18 +53,38 @@ namespace StardewModdingAPI.Framework.Deprecations
/// <param name="logStackTrace">Whether to log a stack trace showing where the deprecated code is in the mod.</param>
public void Warn(IModMetadata? source, string nounPhrase, string version, DeprecationLevel severity, string[]? unlessStackIncludes = null, bool logStackTrace = true)
{
// get heuristic source
// The call stack is usually the most reliable way to get the source if it's unknown. This is *very* slow
// though, especially before we check whether this is a duplicate warning. The initial cache check uses a
// quick heuristic method if at all possible to avoid that.
IModMetadata? heuristicSource = source;
ImmutableStackTrace? stack = null;
if (heuristicSource is null)
Context.HeuristicModsRunningCode.TryPeek(out heuristicSource);
if (heuristicSource is null)
{
stack = ImmutableStackTrace.Get(skipFrames: 1);
heuristicSource = this.ModRegistry.GetFromStack(stack.GetFrames());
}
// skip if already warned
string cacheKey = $"{source?.DisplayName ?? "<unknown>"}::{nounPhrase}::{version}";
string cacheKey = $"{heuristicSource?.Manifest.UniqueID ?? "<unknown>"}::{nounPhrase}::{version}";
if (this.LoggedDeprecations.Contains(cacheKey))
return;
this.LoggedDeprecations.Add(cacheKey);
// warn if valid
ImmutableStackTrace stack = ImmutableStackTrace.Get(skipFrames: 1);
if (!this.ShouldSuppress(stack, unlessStackIncludes))
// get more accurate source
if (stack is not null)
source ??= heuristicSource!;
else
{
this.LoggedDeprecations.Add(cacheKey);
this.QueuedWarnings.Add(new DeprecationWarning(source, nounPhrase, version, severity, stack, logStackTrace));
stack ??= ImmutableStackTrace.Get(skipFrames: 1);
source ??= this.ModRegistry.GetFromStack(stack.GetFrames());
}
// log unless suppressed
if (!this.ShouldSuppress(stack, unlessStackIncludes))
this.QueuedWarnings.Add(new DeprecationWarning(source, nounPhrase, version, severity, stack, logStackTrace));
}
/// <summary>A placeholder method used to track deprecated code for which a separate warning will be shown.</summary>

View File

@ -104,6 +104,8 @@ namespace StardewModdingAPI.Framework.Events
// raise event
foreach (ManagedEventHandler<TEventArgs> handler in this.GetHandlers())
{
Context.HeuristicModsRunningCode.Push(handler.SourceMod);
try
{
handler.Handler(null, args);
@ -112,6 +114,10 @@ namespace StardewModdingAPI.Framework.Events
{
this.LogError(handler, ex);
}
finally
{
Context.HeuristicModsRunningCode.TryPop(out _);
}
}
}
@ -126,6 +132,8 @@ namespace StardewModdingAPI.Framework.Events
// raise event
foreach (ManagedEventHandler<TEventArgs> handler in this.GetHandlers())
{
Context.HeuristicModsRunningCode.Push(handler.SourceMod);
try
{
invoke(handler.SourceMod, args => handler.Handler(null, args));
@ -134,6 +142,10 @@ namespace StardewModdingAPI.Framework.Events
{
this.LogError(handler, ex);
}
finally
{
Context.HeuristicModsRunningCode.TryPop(out _);
}
}
}

View File

@ -511,11 +511,6 @@ namespace StardewModdingAPI.Framework.Logging
"These mods have no update keys in their manifest. SMAPI may not notify you about updates for these",
"mods. Consider notifying the mod authors about this problem."
);
// not crossplatform
this.LogModWarningGroup(modsWithWarnings, ModWarning.UsesDynamic, LogLevel.Debug, "Not crossplatform",
"These mods use the 'dynamic' keyword, and won't work on Linux/macOS."
);
}
}

View File

@ -37,7 +37,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
public bool Trigger(string name, string[] arguments)
{
SCore.DeprecationManager.Warn(
source: SCore.DeprecationManager.GetMod(this.ModID),
source: this.Mod,
nounPhrase: $"{nameof(IModHelper)}.{nameof(IModHelper.ConsoleCommands)}.{nameof(ICommandHelper.Trigger)}",
version: "3.8.1",
severity: DeprecationLevel.Notice

View File

@ -33,7 +33,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
get
{
SCore.DeprecationManager.Warn(
source: SCore.DeprecationManager.GetMod(this.ModID),
source: this.Mod,
nounPhrase: $"{nameof(IModHelper)}.{nameof(IModHelper.Content)}",
version: "3.14.0",
severity: DeprecationLevel.Notice

View File

@ -414,11 +414,6 @@ namespace StardewModdingAPI.Framework.ModLoading
mod.SetWarning(ModWarning.UsesUnvalidatedUpdateTick);
break;
case InstructionHandleResult.DetectedDynamic:
template = $"{logPrefix}Detected 'dynamic' keyword ($phrase) in assembly {filename}.";
mod.SetWarning(ModWarning.UsesDynamic);
break;
case InstructionHandleResult.DetectedConsoleAccess:
template = $"{logPrefix}Detected direct console access ($phrase) in assembly {filename}.";
mod.SetWarning(ModWarning.AccessesConsole);

View File

@ -20,9 +20,6 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <summary>The instruction is compatible, but affects the save serializer in a way that may make saves unloadable without the mod.</summary>
DetectedSaveSerializer,
/// <summary>The instruction is compatible, but uses the <c>dynamic</c> keyword which won't work on Linux/macOS.</summary>
DetectedDynamic,
/// <summary>The instruction is compatible, but references <see cref="ISpecializedEvents.UnvalidatedUpdateTicking"/> or <see cref="ISpecializedEvents.UnvalidatedUpdateTicked"/> which may impact stability.</summary>
DetectedUnvalidatedUpdateTick,

View File

@ -99,14 +99,10 @@ namespace StardewModdingAPI.Framework
}
/// <summary>Get the mod metadata from the closest assembly registered as a source of deprecation warnings.</summary>
/// <param name="frames">The call stack to analyze.</param>
/// <returns>Returns the mod's metadata, or <c>null</c> if no registered assemblies were found.</returns>
public IModMetadata? GetFromStack()
public IModMetadata? GetFromStack(StackFrame[] frames)
{
// get stack frames
StackTrace stack = new();
StackFrame[] frames = stack.GetFrames();
// search stack for a source assembly
foreach (StackFrame frame in frames)
{
IModMetadata? mod = this.GetFrom(frame);
@ -114,7 +110,6 @@ namespace StardewModdingAPI.Framework
return mod;
}
// no known assembly found
return null;
}
}

View File

@ -1677,6 +1677,7 @@ namespace StardewModdingAPI.Framework
#pragma warning restore CS0612, CS0618
// call entry method
Context.HeuristicModsRunningCode.Push(metadata);
try
{
IMod mod = metadata.Mod!;
@ -1705,6 +1706,7 @@ namespace StardewModdingAPI.Framework
{
this.Monitor.Log($"Failed loading mod-provided API for {metadata.DisplayName}. Integrations with other mods may not work. Error: {ex.GetLogSummary()}", LogLevel.Error);
}
Context.HeuristicModsRunningCode.TryPop(out _);
}
// unlock mod integrations
@ -1852,7 +1854,7 @@ namespace StardewModdingAPI.Framework
try
{
// get mod instance
if (!this.TryLoadModEntry(modAssembly, out Mod? modEntry, out errorReasonPhrase))
if (!this.TryLoadModEntry(mod, modAssembly, out Mod? modEntry, out errorReasonPhrase))
{
failReason = ModFailReason.LoadFailed;
return false;
@ -1954,11 +1956,12 @@ namespace StardewModdingAPI.Framework
}
/// <summary>Load a mod's entry class.</summary>
/// <param name="metadata">The mod metadata whose entry class is being loaded.</param>
/// <param name="modAssembly">The mod assembly.</param>
/// <param name="mod">The loaded instance.</param>
/// <param name="error">The error indicating why loading failed (if applicable).</param>
/// <returns>Returns whether the mod entry class was successfully loaded.</returns>
private bool TryLoadModEntry(Assembly modAssembly, [NotNullWhen(true)] out Mod? mod, [NotNullWhen(false)] out string? error)
private bool TryLoadModEntry(IModMetadata metadata, Assembly modAssembly, [NotNullWhen(true)] out Mod? mod, [NotNullWhen(false)] out string? error)
{
mod = null;
@ -1976,7 +1979,16 @@ namespace StardewModdingAPI.Framework
}
// get implementation
mod = (Mod?)modAssembly.CreateInstance(modEntries[0].ToString());
Context.HeuristicModsRunningCode.Push(metadata);
try
{
mod = (Mod?)modAssembly.CreateInstance(modEntries[0].ToString());
}
finally
{
Context.HeuristicModsRunningCode.TryPop(out _);
}
if (mod == null)
{
error = "its entry class couldn't be instantiated.";

View File

@ -67,7 +67,6 @@ namespace StardewModdingAPI.Metadata
/****
** detect code which may impact game stability
****/
yield return new TypeFinder("System.Runtime.CompilerServices.CallSite", InstructionHandleResult.DetectedDynamic);
yield return new FieldFinder(typeof(SaveGame).FullName!, new[] { nameof(SaveGame.serializer), nameof(SaveGame.farmerSerializer), nameof(SaveGame.locationSerializer) }, InstructionHandleResult.DetectedSaveSerializer);
yield return new EventFinder(typeof(ISpecializedEvents).FullName!, new[] { nameof(ISpecializedEvents.UnvalidatedUpdateTicked), nameof(ISpecializedEvents.UnvalidatedUpdateTicking) }, InstructionHandleResult.DetectedUnvalidatedUpdateTick);

View File

@ -97,7 +97,7 @@ namespace StardewModdingAPI.Utilities
if (!nullExpected)
{
SCore.DeprecationManager.Warn(
SCore.DeprecationManager.GetModFromStack(),
null,
$"calling the {nameof(PerScreen<T>)} constructor with null",
"3.14.0",
DeprecationLevel.Notice