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

View File

@ -1,6 +1,16 @@
← [README](README.md) ← [README](README.md)
# Release notes # 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 ## 3.14.3
Released 12 May 2022 for Stardew Valley 1.5.6 or later. Released 12 May 2022 for Stardew Valley 1.5.6 or later.

View File

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

View File

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

View File

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

View File

@ -19,6 +19,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
PatchesGame = 4, PatchesGame = 4,
/// <summary>The mod uses the <c>dynamic</c> keyword which won't work on Linux/macOS.</summary> /// <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, UsesDynamic = 8,
/// <summary>The mod references specialized 'unvalidated update tick' events which may impact stability.</summary> /// <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> /// <summary>Extract mod metadata from the wiki compatibility list.</summary>
public async Task<WikiModList> GetWikiCompatibilityListAsync() public async Task<WikiModList> GetWikiCompatibilityListAsync()
{ {
WikiClient client = new(this.UserAgent); using WikiClient client = new(this.UserAgent);
return await client.FetchModsAsync(); return await client.FetchModsAsync();
} }

View File

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

View File

@ -1,6 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using StardewModdingAPI.Enums; using StardewModdingAPI.Enums;
using StardewModdingAPI.Events; using StardewModdingAPI.Events;
using StardewModdingAPI.Framework;
using StardewModdingAPI.Utilities; using StardewModdingAPI.Utilities;
using StardewValley; using StardewValley;
using StardewValley.Menus; 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> /// <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; 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 ** Accessors

View File

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

View File

@ -64,6 +64,7 @@ namespace StardewModdingAPI.Framework.Content
// check edit // check edit
if (this.Instance is IAssetEditor editor) if (this.Instance is IAssetEditor editor)
{ {
Context.HeuristicModsRunningCode.Push(this.Mod);
try try
{ {
if (editor.CanEdit<TAsset>(asset)) 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); 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 // check load
if (this.Instance is IAssetLoader loader) if (this.Instance is IAssetLoader loader)
{ {
Context.HeuristicModsRunningCode.Push(this.Mod);
try try
{ {
if (loader.CanLoad<TAsset>(asset)) 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); 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; return false;

View File

@ -595,6 +595,7 @@ namespace StardewModdingAPI.Framework
foreach (ModLinked<IAssetLoader> loader in this.Loaders) foreach (ModLinked<IAssetLoader> loader in this.Loaders)
{ {
// check if loader applies // check if loader applies
Context.HeuristicModsRunningCode.Push(loader.Mod);
try try
{ {
if (!loader.Data.CanLoad<T>(legacyInfo)) 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); 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; continue;
} }
finally
{
Context.HeuristicModsRunningCode.TryPop(out _);
}
// add operation // add operation
group ??= new AssetOperationGroup(new List<AssetLoadOperation>(), new List<AssetEditOperation>()); group ??= new AssetOperationGroup(new List<AssetLoadOperation>(), new List<AssetEditOperation>());
@ -617,9 +622,7 @@ namespace StardewModdingAPI.Framework
Mod: loader.Mod, Mod: loader.Mod,
OnBehalfOf: null, OnBehalfOf: null,
Priority: AssetLoadPriority.Exclusive, Priority: AssetLoadPriority.Exclusive,
GetData: assetInfo => loader.Data.Load<T>( GetData: assetInfo => loader.Data.Load<T>(this.GetLegacyAssetInfo(assetInfo))
this.GetLegacyAssetInfo(assetInfo)
)
) )
) )
); );
@ -629,6 +632,7 @@ namespace StardewModdingAPI.Framework
foreach (var editor in this.Editors) foreach (var editor in this.Editors)
{ {
// check if editor applies // check if editor applies
Context.HeuristicModsRunningCode.Push(editor.Mod);
try try
{ {
if (!editor.Data.CanEdit<T>(legacyInfo)) 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); 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; continue;
} }
finally
{
Context.HeuristicModsRunningCode.TryPop(out _);
}
// HACK // HACK
// //
@ -672,9 +680,7 @@ namespace StardewModdingAPI.Framework
Mod: editor.Mod, Mod: editor.Mod,
OnBehalfOf: null, OnBehalfOf: null,
Priority: priority, Priority: priority,
ApplyEdit: assetData => editor.Data.Edit<T>( ApplyEdit: assetData => editor.Data.Edit<T>(this.GetLegacyAssetData(assetData))
this.GetLegacyAssetData(assetData)
)
) )
) )
); );

View File

@ -177,6 +177,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
// fetch asset from loader // fetch asset from loader
IModMetadata mod = loader.Mod; IModMetadata mod = loader.Mod;
T data; T data;
Context.HeuristicModsRunningCode.Push(loader.Mod);
try try
{ {
data = (T)loader.GetData(info); 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); 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; return null;
} }
finally
{
Context.HeuristicModsRunningCode.TryPop(out _);
}
// return matched asset // return matched asset
return this.TryFixAndValidateLoadedAsset(info, data, loader) return this.TryFixAndValidateLoadedAsset(info, data, loader)
@ -229,6 +234,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
// try edit // try edit
object prevAsset = asset.Data; object prevAsset = asset.Data;
Context.HeuristicModsRunningCode.Push(editor.Mod);
try try
{ {
editor.ApplyEdit(asset); 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); 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 // 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) if (asset.Data == null)
{ {
mod.LogAsMod($"Mod incorrectly set asset '{info.Name}'{this.GetOnBehalfOfLabel(editor.OnBehalfOf)} to a null value; ignoring override.", LogLevel.Warn); 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) 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 // track & return asset

View File

@ -37,13 +37,6 @@ namespace StardewModdingAPI.Framework.Deprecations
this.ModRegistry = modRegistry; 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> /// <summary>Get a mod from its unique ID.</summary>
/// <param name="modId">The mod's unique ID.</param> /// <param name="modId">The mod's unique ID.</param>
public IModMetadata? GetMod(string modId) public IModMetadata? GetMod(string modId)
@ -52,7 +45,7 @@ namespace StardewModdingAPI.Framework.Deprecations
} }
/// <summary>Log a deprecation warning.</summary> /// <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="nounPhrase">A noun phrase describing what is deprecated.</param>
/// <param name="version">The SMAPI version which deprecated it.</param> /// <param name="version">The SMAPI version which deprecated it.</param>
/// <param name="severity">How deprecated the code is.</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> /// <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) 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 // skip if already warned
string cacheKey = $"{source?.DisplayName ?? "<unknown>"}::{nounPhrase}::{version}"; string cacheKey = $"{heuristicSource?.Manifest.UniqueID ?? "<unknown>"}::{nounPhrase}::{version}";
if (this.LoggedDeprecations.Contains(cacheKey)) if (this.LoggedDeprecations.Contains(cacheKey))
return; return;
this.LoggedDeprecations.Add(cacheKey);
// warn if valid // get more accurate source
ImmutableStackTrace stack = ImmutableStackTrace.Get(skipFrames: 1); if (stack is not null)
if (!this.ShouldSuppress(stack, unlessStackIncludes)) source ??= heuristicSource!;
else
{ {
this.LoggedDeprecations.Add(cacheKey); stack ??= ImmutableStackTrace.Get(skipFrames: 1);
this.QueuedWarnings.Add(new DeprecationWarning(source, nounPhrase, version, severity, stack, logStackTrace)); 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> /// <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 // raise event
foreach (ManagedEventHandler<TEventArgs> handler in this.GetHandlers()) foreach (ManagedEventHandler<TEventArgs> handler in this.GetHandlers())
{ {
Context.HeuristicModsRunningCode.Push(handler.SourceMod);
try try
{ {
handler.Handler(null, args); handler.Handler(null, args);
@ -112,6 +114,10 @@ namespace StardewModdingAPI.Framework.Events
{ {
this.LogError(handler, ex); this.LogError(handler, ex);
} }
finally
{
Context.HeuristicModsRunningCode.TryPop(out _);
}
} }
} }
@ -126,6 +132,8 @@ namespace StardewModdingAPI.Framework.Events
// raise event // raise event
foreach (ManagedEventHandler<TEventArgs> handler in this.GetHandlers()) foreach (ManagedEventHandler<TEventArgs> handler in this.GetHandlers())
{ {
Context.HeuristicModsRunningCode.Push(handler.SourceMod);
try try
{ {
invoke(handler.SourceMod, args => handler.Handler(null, args)); invoke(handler.SourceMod, args => handler.Handler(null, args));
@ -134,6 +142,10 @@ namespace StardewModdingAPI.Framework.Events
{ {
this.LogError(handler, ex); 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", "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." "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) public bool Trigger(string name, string[] arguments)
{ {
SCore.DeprecationManager.Warn( SCore.DeprecationManager.Warn(
source: SCore.DeprecationManager.GetMod(this.ModID), source: this.Mod,
nounPhrase: $"{nameof(IModHelper)}.{nameof(IModHelper.ConsoleCommands)}.{nameof(ICommandHelper.Trigger)}", nounPhrase: $"{nameof(IModHelper)}.{nameof(IModHelper.ConsoleCommands)}.{nameof(ICommandHelper.Trigger)}",
version: "3.8.1", version: "3.8.1",
severity: DeprecationLevel.Notice severity: DeprecationLevel.Notice

View File

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

View File

@ -414,11 +414,6 @@ namespace StardewModdingAPI.Framework.ModLoading
mod.SetWarning(ModWarning.UsesUnvalidatedUpdateTick); mod.SetWarning(ModWarning.UsesUnvalidatedUpdateTick);
break; break;
case InstructionHandleResult.DetectedDynamic:
template = $"{logPrefix}Detected 'dynamic' keyword ($phrase) in assembly {filename}.";
mod.SetWarning(ModWarning.UsesDynamic);
break;
case InstructionHandleResult.DetectedConsoleAccess: case InstructionHandleResult.DetectedConsoleAccess:
template = $"{logPrefix}Detected direct console access ($phrase) in assembly {filename}."; template = $"{logPrefix}Detected direct console access ($phrase) in assembly {filename}.";
mod.SetWarning(ModWarning.AccessesConsole); 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> /// <summary>The instruction is compatible, but affects the save serializer in a way that may make saves unloadable without the mod.</summary>
DetectedSaveSerializer, 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> /// <summary>The instruction is compatible, but references <see cref="ISpecializedEvents.UnvalidatedUpdateTicking"/> or <see cref="ISpecializedEvents.UnvalidatedUpdateTicked"/> which may impact stability.</summary>
DetectedUnvalidatedUpdateTick, 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> /// <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> /// <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) foreach (StackFrame frame in frames)
{ {
IModMetadata? mod = this.GetFrom(frame); IModMetadata? mod = this.GetFrom(frame);
@ -114,7 +110,6 @@ namespace StardewModdingAPI.Framework
return mod; return mod;
} }
// no known assembly found
return null; return null;
} }
} }

View File

@ -1677,6 +1677,7 @@ namespace StardewModdingAPI.Framework
#pragma warning restore CS0612, CS0618 #pragma warning restore CS0612, CS0618
// call entry method // call entry method
Context.HeuristicModsRunningCode.Push(metadata);
try try
{ {
IMod mod = metadata.Mod!; 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); 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 // unlock mod integrations
@ -1852,7 +1854,7 @@ namespace StardewModdingAPI.Framework
try try
{ {
// get mod instance // 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; failReason = ModFailReason.LoadFailed;
return false; return false;
@ -1954,11 +1956,12 @@ namespace StardewModdingAPI.Framework
} }
/// <summary>Load a mod's entry class.</summary> /// <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="modAssembly">The mod assembly.</param>
/// <param name="mod">The loaded instance.</param> /// <param name="mod">The loaded instance.</param>
/// <param name="error">The error indicating why loading failed (if applicable).</param> /// <param name="error">The error indicating why loading failed (if applicable).</param>
/// <returns>Returns whether the mod entry class was successfully loaded.</returns> /// <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; mod = null;
@ -1976,7 +1979,16 @@ namespace StardewModdingAPI.Framework
} }
// get implementation // 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) if (mod == null)
{ {
error = "its entry class couldn't be instantiated."; error = "its entry class couldn't be instantiated.";

View File

@ -67,7 +67,6 @@ namespace StardewModdingAPI.Metadata
/**** /****
** detect code which may impact game stability ** 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 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); 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) if (!nullExpected)
{ {
SCore.DeprecationManager.Warn( SCore.DeprecationManager.Warn(
SCore.DeprecationManager.GetModFromStack(), null,
$"calling the {nameof(PerScreen<T>)} constructor with null", $"calling the {nameof(PerScreen<T>)} constructor with null",
"3.14.0", "3.14.0",
DeprecationLevel.Notice DeprecationLevel.Notice