Merge branch 'develop' into stable
This commit is contained in:
commit
e10147e7bd
|
@ -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.6</Version>
|
<Version>3.14.7</Version>
|
||||||
<Product>SMAPI</Product>
|
<Product>SMAPI</Product>
|
||||||
<LangVersion>latest</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
<AssemblySearchPaths>$(AssemblySearchPaths);{GAC}</AssemblySearchPaths>
|
<AssemblySearchPaths>$(AssemblySearchPaths);{GAC}</AssemblySearchPaths>
|
||||||
|
@ -69,8 +69,10 @@
|
||||||
<Copy SourceFiles="$(TargetDir)\MonoMod.Common.dll" DestinationFolder="$(GamePath)\smapi-internal" />
|
<Copy SourceFiles="$(TargetDir)\MonoMod.Common.dll" DestinationFolder="$(GamePath)\smapi-internal" />
|
||||||
|
|
||||||
<!-- .NET dependencies -->
|
<!-- .NET dependencies -->
|
||||||
<Copy SourceFiles="$(TargetDir)\System.Configuration.ConfigurationManager.dll" DestinationFolder="$(GamePath)\smapi-internal" />
|
|
||||||
<Copy SourceFiles="$(TargetDir)\System.Management.dll" DestinationFolder="$(GamePath)\smapi-internal" Condition="$(OS) == 'Windows_NT'" />
|
<Copy SourceFiles="$(TargetDir)\System.Management.dll" DestinationFolder="$(GamePath)\smapi-internal" Condition="$(OS) == 'Windows_NT'" />
|
||||||
|
|
||||||
|
<!-- Legacy .NET dependencies (remove in SMAPI 4.0.0) -->
|
||||||
|
<Copy SourceFiles="$(TargetDir)\System.Configuration.ConfigurationManager.dll" DestinationFolder="$(GamePath)\smapi-internal" />
|
||||||
<Copy SourceFiles="$(TargetDir)\System.Runtime.Caching.dll" DestinationFolder="$(GamePath)\smapi-internal" />
|
<Copy SourceFiles="$(TargetDir)\System.Runtime.Caching.dll" DestinationFolder="$(GamePath)\smapi-internal" />
|
||||||
<Copy SourceFiles="$(TargetDir)\System.Security.Permissions.dll" DestinationFolder="$(GamePath)\smapi-internal" />
|
<Copy SourceFiles="$(TargetDir)\System.Security.Permissions.dll" DestinationFolder="$(GamePath)\smapi-internal" />
|
||||||
</Target>
|
</Target>
|
||||||
|
|
|
@ -142,19 +142,20 @@ for folder in ${folders[@]}; do
|
||||||
cp "$smapiBin/SMAPI.metadata.json" "$bundlePath/smapi-internal/metadata.json"
|
cp "$smapiBin/SMAPI.metadata.json" "$bundlePath/smapi-internal/metadata.json"
|
||||||
if [ $folder == "linux" ] || [ $folder == "macOS" ]; then
|
if [ $folder == "linux" ] || [ $folder == "macOS" ]; then
|
||||||
cp "$installAssets/unix-launcher.sh" "$bundlePath"
|
cp "$installAssets/unix-launcher.sh" "$bundlePath"
|
||||||
cp "$smapiBin/System.Runtime.Caching.dll" "$bundlePath/smapi-internal"
|
|
||||||
else
|
else
|
||||||
cp "$installAssets/windows-exe-config.xml" "$bundlePath/StardewModdingAPI.exe.config"
|
cp "$installAssets/windows-exe-config.xml" "$bundlePath/StardewModdingAPI.exe.config"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# copy .NET dependencies
|
# copy .NET dependencies
|
||||||
cp "$smapiBin/System.Configuration.ConfigurationManager.dll" "$bundlePath/smapi-internal"
|
|
||||||
cp "$smapiBin/System.Runtime.Caching.dll" "$bundlePath/smapi-internal"
|
|
||||||
cp "$smapiBin/System.Security.Permissions.dll" "$bundlePath/smapi-internal"
|
|
||||||
if [ $folder == "windows" ]; then
|
if [ $folder == "windows" ]; then
|
||||||
cp "$smapiBin/System.Management.dll" "$bundlePath/smapi-internal"
|
cp "$smapiBin/System.Management.dll" "$bundlePath/smapi-internal"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# copy legacy .NET dependencies (remove in SMAPI 4.0.0)
|
||||||
|
cp "$smapiBin/System.Configuration.ConfigurationManager.dll" "$bundlePath/smapi-internal"
|
||||||
|
cp "$smapiBin/System.Runtime.Caching.dll" "$bundlePath/smapi-internal"
|
||||||
|
cp "$smapiBin/System.Security.Permissions.dll" "$bundlePath/smapi-internal"
|
||||||
|
|
||||||
# copy bundled mods
|
# copy bundled mods
|
||||||
for modName in ${bundleModNames[@]}; do
|
for modName in ${bundleModNames[@]}; do
|
||||||
fromPath="src/SMAPI.Mods.$modName/bin/$buildConfig/$runtime/publish"
|
fromPath="src/SMAPI.Mods.$modName/bin/$buildConfig/$runtime/publish"
|
||||||
|
|
|
@ -162,20 +162,21 @@ foreach ($folder in $folders) {
|
||||||
cp "$smapiBin/SMAPI.metadata.json" "$bundlePath/smapi-internal/metadata.json"
|
cp "$smapiBin/SMAPI.metadata.json" "$bundlePath/smapi-internal/metadata.json"
|
||||||
if ($folder -eq "linux" -or $folder -eq "macOS") {
|
if ($folder -eq "linux" -or $folder -eq "macOS") {
|
||||||
cp "$installAssets/unix-launcher.sh" "$bundlePath"
|
cp "$installAssets/unix-launcher.sh" "$bundlePath"
|
||||||
cp "$smapiBin/System.Runtime.Caching.dll" "$bundlePath/smapi-internal"
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
cp "$installAssets/windows-exe-config.xml" "$bundlePath/StardewModdingAPI.exe.config"
|
cp "$installAssets/windows-exe-config.xml" "$bundlePath/StardewModdingAPI.exe.config"
|
||||||
}
|
}
|
||||||
|
|
||||||
# copy .NET dependencies
|
# copy .NET dependencies
|
||||||
cp "$smapiBin/System.Configuration.ConfigurationManager.dll" "$bundlePath/smapi-internal"
|
|
||||||
cp "$smapiBin/System.Runtime.Caching.dll" "$bundlePath/smapi-internal"
|
|
||||||
cp "$smapiBin/System.Security.Permissions.dll" "$bundlePath/smapi-internal"
|
|
||||||
if ($folder -eq "windows") {
|
if ($folder -eq "windows") {
|
||||||
cp "$smapiBin/System.Management.dll" "$bundlePath/smapi-internal"
|
cp "$smapiBin/System.Management.dll" "$bundlePath/smapi-internal"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# copy legacy .NET dependencies (remove in SMAPI 4.0.0)
|
||||||
|
cp "$smapiBin/System.Configuration.ConfigurationManager.dll" "$bundlePath/smapi-internal"
|
||||||
|
cp "$smapiBin/System.Runtime.Caching.dll" "$bundlePath/smapi-internal"
|
||||||
|
cp "$smapiBin/System.Security.Permissions.dll" "$bundlePath/smapi-internal"
|
||||||
|
|
||||||
# copy bundled mods
|
# copy bundled mods
|
||||||
foreach ($modName in $bundleModNames) {
|
foreach ($modName in $bundleModNames) {
|
||||||
$fromPath = "src/SMAPI.Mods.$modName/bin/$buildConfig/$runtime/publish"
|
$fromPath = "src/SMAPI.Mods.$modName/bin/$buildConfig/$runtime/publish"
|
||||||
|
|
|
@ -1,6 +1,14 @@
|
||||||
← [README](README.md)
|
← [README](README.md)
|
||||||
|
|
||||||
# Release notes
|
# Release notes
|
||||||
|
## 3.14.7
|
||||||
|
Released 01 June 2022 for Stardew Valley 1.5.6 or later.
|
||||||
|
|
||||||
|
* For players:
|
||||||
|
* Optimized reflection cache to reduce frame skips for some players.
|
||||||
|
* For mod authors:
|
||||||
|
* Removed `runtimeconfig.json` setting which impacted hot reload support.
|
||||||
|
|
||||||
## 3.14.6
|
## 3.14.6
|
||||||
Released 27 May 2022 for Stardew Valley 1.5.6 or later.
|
Released 27 May 2022 for Stardew Valley 1.5.6 or later.
|
||||||
|
|
||||||
|
|
|
@ -9,8 +9,9 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"configProperties": {
|
"configProperties": {
|
||||||
"System.Runtime.TieredCompilation": false,
|
// disable tiered runtime JIT: https://github.com/dotnet/runtime/blob/main/docs/design/features/tiered-compilation.md
|
||||||
"System.Reflection.Metadata.MetadataUpdater.IsSupported": false
|
// This is disabled by the base game, and causes issues with Harmony patches.
|
||||||
|
"System.Runtime.TieredCompilation": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
{
|
{
|
||||||
"Name": "Console Commands",
|
"Name": "Console Commands",
|
||||||
"Author": "SMAPI",
|
"Author": "SMAPI",
|
||||||
"Version": "3.14.6",
|
"Version": "3.14.7",
|
||||||
"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.6"
|
"MinimumApiVersion": "3.14.7"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
{
|
{
|
||||||
"Name": "Error Handler",
|
"Name": "Error Handler",
|
||||||
"Author": "SMAPI",
|
"Author": "SMAPI",
|
||||||
"Version": "3.14.6",
|
"Version": "3.14.7",
|
||||||
"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.6"
|
"MinimumApiVersion": "3.14.7"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
{
|
{
|
||||||
"Name": "Save Backup",
|
"Name": "Save Backup",
|
||||||
"Author": "SMAPI",
|
"Author": "SMAPI",
|
||||||
"Version": "3.14.6",
|
"Version": "3.14.7",
|
||||||
"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.6"
|
"MinimumApiVersion": "3.14.7"
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,15 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
|
||||||
AccessesFilesystem = 128,
|
AccessesFilesystem = 128,
|
||||||
|
|
||||||
/// <summary>Uses .NET APIs for shell or process access.</summary>
|
/// <summary>Uses .NET APIs for shell or process access.</summary>
|
||||||
AccessesShell = 256
|
AccessesShell = 256,
|
||||||
|
|
||||||
|
/// <summary>References the legacy <c>System.Configuration.ConfigurationManager</c> assembly and doesn't include a copy in the mod folder, so it'll break in SMAPI 4.0.0.</summary>
|
||||||
|
DetectedLegacyConfigurationDll = 512,
|
||||||
|
|
||||||
|
/// <summary>References the legacy <c>System.Runtime.Caching</c> assembly and doesn't include a copy in the mod folder, so it'll break in SMAPI 4.0.0.</summary>
|
||||||
|
DetectedLegacyCachingDll = 1024,
|
||||||
|
|
||||||
|
/// <summary>References the legacy <c>System.Security.Permissions</c> assembly and doesn't include a copy in the mod folder, so it'll break in SMAPI 4.0.0.</summary>
|
||||||
|
DetectedLegacyPermissionsDll = 2048
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.6";
|
internal static string RawApiVersion = "3.14.7";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Contains SMAPI's constants and assumptions.</summary>
|
/// <summary>Contains SMAPI's constants and assumptions.</summary>
|
||||||
|
|
|
@ -88,6 +88,10 @@ namespace StardewModdingAPI.Framework
|
||||||
/// <param name="warning">The warning to set.</param>
|
/// <param name="warning">The warning to set.</param>
|
||||||
IModMetadata SetWarning(ModWarning warning);
|
IModMetadata SetWarning(ModWarning warning);
|
||||||
|
|
||||||
|
/// <summary>Remove a warning flag for the mod.</summary>
|
||||||
|
/// <param name="warning">The warning to remove.</param>
|
||||||
|
IModMetadata RemoveWarning(ModWarning warning);
|
||||||
|
|
||||||
/// <summary>Set the mod instance.</summary>
|
/// <summary>Set the mod instance.</summary>
|
||||||
/// <param name="mod">The mod instance to set.</param>
|
/// <param name="mod">The mod instance to set.</param>
|
||||||
/// <param name="translations">The translations for this mod (if loaded).</param>
|
/// <param name="translations">The translations for this mod (if loaded).</param>
|
||||||
|
|
|
@ -163,6 +163,29 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||||
this.AssemblyDefinitionResolver.Add(assembly.Definition);
|
this.AssemblyDefinitionResolver.Add(assembly.Definition);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// special case: clear legacy-DLL warnings if the mod bundles a copy
|
||||||
|
if (mod.Warnings.HasFlag(ModWarning.DetectedLegacyCachingDll))
|
||||||
|
{
|
||||||
|
if (File.Exists(Path.Combine(mod.DirectoryPath, "System.Runtime.Caching.dll")))
|
||||||
|
mod.RemoveWarning(ModWarning.DetectedLegacyCachingDll);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// remove duplicate warnings (System.Runtime.Caching.dll references these)
|
||||||
|
mod.RemoveWarning(ModWarning.DetectedLegacyConfigurationDll);
|
||||||
|
mod.RemoveWarning(ModWarning.DetectedLegacyPermissionsDll);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mod.Warnings.HasFlag(ModWarning.DetectedLegacyConfigurationDll))
|
||||||
|
{
|
||||||
|
if (File.Exists(Path.Combine(mod.DirectoryPath, "System.Configuration.ConfigurationManager.dll")))
|
||||||
|
mod.RemoveWarning(ModWarning.DetectedLegacyConfigurationDll);
|
||||||
|
}
|
||||||
|
if (mod.Warnings.HasFlag(ModWarning.DetectedLegacyPermissionsDll))
|
||||||
|
{
|
||||||
|
if (File.Exists(Path.Combine(mod.DirectoryPath, "System.Security.Permissions.dll")))
|
||||||
|
mod.RemoveWarning(ModWarning.DetectedLegacyPermissionsDll);
|
||||||
|
}
|
||||||
|
|
||||||
// throw if incompatibilities detected
|
// throw if incompatibilities detected
|
||||||
if (!assumeCompatible && mod.Warnings.HasFlag(ModWarning.BrokenCodeLoaded))
|
if (!assumeCompatible && mod.Warnings.HasFlag(ModWarning.BrokenCodeLoaded))
|
||||||
throw new IncompatibleInstructionException();
|
throw new IncompatibleInstructionException();
|
||||||
|
@ -429,6 +452,21 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||||
mod.SetWarning(ModWarning.AccessesShell);
|
mod.SetWarning(ModWarning.AccessesShell);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case InstructionHandleResult.DetectedLegacyCachingDll:
|
||||||
|
template = $"{logPrefix}Detected reference to System.Runtime.Caching.dll, which will be removed in SMAPI 4.0.0.";
|
||||||
|
mod.SetWarning(ModWarning.DetectedLegacyCachingDll);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case InstructionHandleResult.DetectedLegacyConfigurationDll:
|
||||||
|
template = $"{logPrefix}Detected reference to System.Configuration.ConfigurationManager.dll, which will be removed in SMAPI 4.0.0.";
|
||||||
|
mod.SetWarning(ModWarning.DetectedLegacyConfigurationDll);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case InstructionHandleResult.DetectedLegacyPermissionsDll:
|
||||||
|
template = $"{logPrefix}Detected reference to System.Security.Permissions.dll, which will be removed in SMAPI 4.0.0.";
|
||||||
|
mod.SetWarning(ModWarning.DetectedLegacyPermissionsDll);
|
||||||
|
break;
|
||||||
|
|
||||||
case InstructionHandleResult.None:
|
case InstructionHandleResult.None:
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
using Mono.Cecil;
|
||||||
|
using StardewModdingAPI.Framework.ModLoading.Framework;
|
||||||
|
|
||||||
|
namespace StardewModdingAPI.Framework.ModLoading.Finders
|
||||||
|
{
|
||||||
|
/// <summary>Detects assembly references which will break in SMAPI 4.0.0.</summary>
|
||||||
|
internal class LegacyAssemblyFinder : BaseInstructionHandler
|
||||||
|
{
|
||||||
|
/*********
|
||||||
|
** Public methods
|
||||||
|
*********/
|
||||||
|
/// <summary>Construct an instance.</summary>
|
||||||
|
public LegacyAssemblyFinder()
|
||||||
|
: base(defaultPhrase: "legacy assembly references") { }
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override bool Handle(ModuleDefinition module)
|
||||||
|
{
|
||||||
|
foreach (AssemblyNameReference assembly in module.AssemblyReferences)
|
||||||
|
{
|
||||||
|
InstructionHandleResult flag = this.GetFlag(assembly);
|
||||||
|
if (flag is InstructionHandleResult.None)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
this.MarkFlag(flag);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*********
|
||||||
|
** Private methods
|
||||||
|
*********/
|
||||||
|
/// <summary>Get the instruction handle flag for the given assembly reference, if any.</summary>
|
||||||
|
/// <param name="assemblyRef">The assembly reference.</param>
|
||||||
|
private InstructionHandleResult GetFlag(AssemblyNameReference assemblyRef)
|
||||||
|
{
|
||||||
|
return assemblyRef.Name switch
|
||||||
|
{
|
||||||
|
"System.Configuration.ConfigurationManager" => InstructionHandleResult.DetectedLegacyConfigurationDll,
|
||||||
|
"System.Runtime.Caching" => InstructionHandleResult.DetectedLegacyCachingDll,
|
||||||
|
"System.Security.Permission" => InstructionHandleResult.DetectedLegacyPermissionsDll,
|
||||||
|
_ => InstructionHandleResult.None
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -30,6 +30,15 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||||
DetectedFilesystemAccess,
|
DetectedFilesystemAccess,
|
||||||
|
|
||||||
/// <summary>The instruction accesses the OS shell or processes directly.</summary>
|
/// <summary>The instruction accesses the OS shell or processes directly.</summary>
|
||||||
DetectedShellAccess
|
DetectedShellAccess,
|
||||||
|
|
||||||
|
/// <summary>The module references the legacy <c>System.Configuration.ConfigurationManager</c> assembly and doesn't include a copy in the mod folder, so it'll break in SMAPI 4.0.0.</summary>
|
||||||
|
DetectedLegacyConfigurationDll,
|
||||||
|
|
||||||
|
/// <summary>The module references the legacy <c>System.Runtime.Caching</c> assembly and doesn't include a copy in the mod folder, so it'll break in SMAPI 4.0.0.</summary>
|
||||||
|
DetectedLegacyCachingDll,
|
||||||
|
|
||||||
|
/// <summary>The module references the legacy <c>System.Security.Permissions</c> assembly and doesn't include a copy in the mod folder, so it'll break in SMAPI 4.0.0.</summary>
|
||||||
|
DetectedLegacyPermissionsDll
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -138,6 +138,13 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IModMetadata RemoveWarning(ModWarning warning)
|
||||||
|
{
|
||||||
|
this.ActualWarnings &= ~warning;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IModMetadata SetMod(IMod mod, TranslationHelper translations)
|
public IModMetadata SetMod(IMod mod, TranslationHelper translations)
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.Reflection;
|
|
||||||
|
|
||||||
namespace StardewModdingAPI.Framework.Reflection
|
|
||||||
{
|
|
||||||
/// <summary>A cached member reflection result.</summary>
|
|
||||||
internal readonly struct CacheEntry
|
|
||||||
{
|
|
||||||
/*********
|
|
||||||
** Accessors
|
|
||||||
*********/
|
|
||||||
/// <summary>Whether the lookup found a valid match.</summary>
|
|
||||||
[MemberNotNullWhen(true, nameof(CacheEntry.MemberInfo))]
|
|
||||||
public bool IsValid => this.MemberInfo != null;
|
|
||||||
|
|
||||||
/// <summary>The reflection data for this member (or <c>null</c> if invalid).</summary>
|
|
||||||
public MemberInfo? MemberInfo { get; }
|
|
||||||
|
|
||||||
|
|
||||||
/*********
|
|
||||||
** Public methods
|
|
||||||
*********/
|
|
||||||
/// <summary>Construct an instance.</summary>
|
|
||||||
/// <param name="memberInfo">The reflection data for this member (or <c>null</c> if invalid).</param>
|
|
||||||
public CacheEntry(MemberInfo? memberInfo)
|
|
||||||
{
|
|
||||||
this.MemberInfo = memberInfo;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.Caching;
|
using StardewModdingAPI.Framework.Utilities;
|
||||||
|
|
||||||
namespace StardewModdingAPI.Framework.Reflection
|
namespace StardewModdingAPI.Framework.Reflection
|
||||||
{
|
{
|
||||||
|
@ -12,10 +12,7 @@ namespace StardewModdingAPI.Framework.Reflection
|
||||||
** Fields
|
** Fields
|
||||||
*********/
|
*********/
|
||||||
/// <summary>The cached fields and methods found via reflection.</summary>
|
/// <summary>The cached fields and methods found via reflection.</summary>
|
||||||
private readonly MemoryCache Cache = new(typeof(Reflector).FullName!);
|
private readonly IntervalMemoryCache<string, MemberInfo?> Cache = new();
|
||||||
|
|
||||||
/// <summary>The sliding cache expiration time.</summary>
|
|
||||||
private readonly TimeSpan SlidingCacheExpiry = TimeSpan.FromMinutes(5);
|
|
||||||
|
|
||||||
|
|
||||||
/*********
|
/*********
|
||||||
|
@ -136,6 +133,15 @@ namespace StardewModdingAPI.Framework.Reflection
|
||||||
return method!;
|
return method!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/****
|
||||||
|
** Management
|
||||||
|
****/
|
||||||
|
/// <summary>Start a new cache interval, clearing stale reflection lookups.</summary>
|
||||||
|
public void NewCacheInterval()
|
||||||
|
{
|
||||||
|
this.Cache.StartNewInterval();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*********
|
/*********
|
||||||
** Private methods
|
** Private methods
|
||||||
|
@ -149,7 +155,9 @@ namespace StardewModdingAPI.Framework.Reflection
|
||||||
private IReflectedField<TValue>? GetFieldFromHierarchy<TValue>(Type type, object? obj, string name, BindingFlags bindingFlags)
|
private IReflectedField<TValue>? GetFieldFromHierarchy<TValue>(Type type, object? obj, string name, BindingFlags bindingFlags)
|
||||||
{
|
{
|
||||||
bool isStatic = bindingFlags.HasFlag(BindingFlags.Static);
|
bool isStatic = bindingFlags.HasFlag(BindingFlags.Static);
|
||||||
FieldInfo? field = this.GetCached($"field::{isStatic}::{type.FullName}::{name}", () =>
|
FieldInfo? field = this.GetCached(
|
||||||
|
'f', type, name, isStatic,
|
||||||
|
fetch: () =>
|
||||||
{
|
{
|
||||||
for (Type? curType = type; curType != null; curType = curType.BaseType)
|
for (Type? curType = type; curType != null; curType = curType.BaseType)
|
||||||
{
|
{
|
||||||
|
@ -162,7 +170,8 @@ namespace StardewModdingAPI.Framework.Reflection
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return field != null
|
return field != null
|
||||||
? new ReflectedField<TValue>(type, obj, field, isStatic)
|
? new ReflectedField<TValue>(type, obj, field, isStatic)
|
||||||
|
@ -178,7 +187,9 @@ namespace StardewModdingAPI.Framework.Reflection
|
||||||
private IReflectedProperty<TValue>? GetPropertyFromHierarchy<TValue>(Type type, object? obj, string name, BindingFlags bindingFlags)
|
private IReflectedProperty<TValue>? GetPropertyFromHierarchy<TValue>(Type type, object? obj, string name, BindingFlags bindingFlags)
|
||||||
{
|
{
|
||||||
bool isStatic = bindingFlags.HasFlag(BindingFlags.Static);
|
bool isStatic = bindingFlags.HasFlag(BindingFlags.Static);
|
||||||
PropertyInfo? property = this.GetCached<PropertyInfo>($"property::{isStatic}::{type.FullName}::{name}", () =>
|
PropertyInfo? property = this.GetCached(
|
||||||
|
'p', type, name, isStatic,
|
||||||
|
fetch: () =>
|
||||||
{
|
{
|
||||||
for (Type? curType = type; curType != null; curType = curType.BaseType)
|
for (Type? curType = type; curType != null; curType = curType.BaseType)
|
||||||
{
|
{
|
||||||
|
@ -191,7 +202,8 @@ namespace StardewModdingAPI.Framework.Reflection
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return property != null
|
return property != null
|
||||||
? new ReflectedProperty<TValue>(type, obj, property, isStatic)
|
? new ReflectedProperty<TValue>(type, obj, property, isStatic)
|
||||||
|
@ -206,7 +218,9 @@ namespace StardewModdingAPI.Framework.Reflection
|
||||||
private IReflectedMethod? GetMethodFromHierarchy(Type type, object? obj, string name, BindingFlags bindingFlags)
|
private IReflectedMethod? GetMethodFromHierarchy(Type type, object? obj, string name, BindingFlags bindingFlags)
|
||||||
{
|
{
|
||||||
bool isStatic = bindingFlags.HasFlag(BindingFlags.Static);
|
bool isStatic = bindingFlags.HasFlag(BindingFlags.Static);
|
||||||
MethodInfo? method = this.GetCached($"method::{isStatic}::{type.FullName}::{name}", () =>
|
MethodInfo? method = this.GetCached(
|
||||||
|
'm', type, name, isStatic,
|
||||||
|
fetch: () =>
|
||||||
{
|
{
|
||||||
for (Type? curType = type; curType != null; curType = curType.BaseType)
|
for (Type? curType = type; curType != null; curType = curType.BaseType)
|
||||||
{
|
{
|
||||||
|
@ -219,34 +233,26 @@ namespace StardewModdingAPI.Framework.Reflection
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return method != null
|
return method != null
|
||||||
? new ReflectedMethod(type, obj, method, isStatic: bindingFlags.HasFlag(BindingFlags.Static))
|
? new ReflectedMethod(type, obj, method, isStatic: isStatic)
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Get a method or field through the cache.</summary>
|
/// <summary>Get a method or field through the cache.</summary>
|
||||||
/// <typeparam name="TMemberInfo">The expected <see cref="MemberInfo"/> type.</typeparam>
|
/// <typeparam name="TMemberInfo">The expected <see cref="MemberInfo"/> type.</typeparam>
|
||||||
/// <param name="key">The cache key.</param>
|
/// <param name="memberType">A letter representing the member type (like 'm' for method).</param>
|
||||||
|
/// <param name="type">The type whose members are being reflected.</param>
|
||||||
|
/// <param name="memberName">The member name.</param>
|
||||||
|
/// <param name="isStatic">Whether the member is static.</param>
|
||||||
/// <param name="fetch">Fetches a new value to cache.</param>
|
/// <param name="fetch">Fetches a new value to cache.</param>
|
||||||
private TMemberInfo? GetCached<TMemberInfo>(string key, Func<TMemberInfo?> fetch)
|
private TMemberInfo? GetCached<TMemberInfo>(char memberType, Type type, string memberName, bool isStatic, Func<TMemberInfo?> fetch)
|
||||||
where TMemberInfo : MemberInfo
|
where TMemberInfo : MemberInfo
|
||||||
{
|
{
|
||||||
// get from cache
|
string key = $"{memberType}{(isStatic ? 's' : 'i')}{type.FullName}:{memberName}";
|
||||||
if (this.Cache.Contains(key))
|
return (TMemberInfo?)this.Cache.GetOrSet(key, fetch);
|
||||||
{
|
|
||||||
CacheEntry entry = (CacheEntry)this.Cache[key];
|
|
||||||
return entry.IsValid
|
|
||||||
? (TMemberInfo)entry.MemberInfo
|
|
||||||
: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
// fetch & cache new value
|
|
||||||
TMemberInfo? result = fetch();
|
|
||||||
CacheEntry cacheEntry = new(result);
|
|
||||||
this.Cache.Add(key, cacheEntry, new CacheItemPolicy { SlidingExpiration = this.SlidingCacheExpiry });
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1164,6 +1164,8 @@ namespace StardewModdingAPI.Framework
|
||||||
protected void OnNewDayAfterFade()
|
protected void OnNewDayAfterFade()
|
||||||
{
|
{
|
||||||
this.EventManager.DayEnding.RaiseEmpty();
|
this.EventManager.DayEnding.RaiseEmpty();
|
||||||
|
|
||||||
|
this.Reflection.NewCacheInterval();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>A callback invoked after an asset is fully loaded through a content manager.</summary>
|
/// <summary>A callback invoked after an asset is fully loaded through a content manager.</summary>
|
||||||
|
@ -1677,6 +1679,31 @@ namespace StardewModdingAPI.Framework
|
||||||
}
|
}
|
||||||
#pragma warning restore CS0612, CS0618
|
#pragma warning restore CS0612, CS0618
|
||||||
|
|
||||||
|
// log deprecation warnings
|
||||||
|
if (metadata.HasWarnings(ModWarning.DetectedLegacyCachingDll, ModWarning.DetectedLegacyConfigurationDll, ModWarning.DetectedLegacyPermissionsDll))
|
||||||
|
{
|
||||||
|
string?[] referenced =
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
metadata.Warnings.HasFlag(ModWarning.DetectedLegacyConfigurationDll) ? "System.Configuration.ConfigurationManager" : null,
|
||||||
|
metadata.Warnings.HasFlag(ModWarning.DetectedLegacyCachingDll) ? "System.Runtime.Caching" : null,
|
||||||
|
metadata.Warnings.HasFlag(ModWarning.DetectedLegacyPermissionsDll) ? "System.Security.Permissions" : null
|
||||||
|
}
|
||||||
|
.Where(p => p is not null)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
foreach (string? name in referenced)
|
||||||
|
{
|
||||||
|
DeprecationManager.Warn(
|
||||||
|
metadata,
|
||||||
|
$"using {name} without bundling it",
|
||||||
|
"3.14.7",
|
||||||
|
DeprecationLevel.Notice,
|
||||||
|
logStackTrace: false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// call entry method
|
// call entry method
|
||||||
Context.HeuristicModsRunningCode.Push(metadata);
|
Context.HeuristicModsRunningCode.Push(metadata);
|
||||||
try
|
try
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace StardewModdingAPI.Framework.Utilities
|
||||||
|
{
|
||||||
|
/// <summary>A memory cache with sliding expiry based on custom intervals, with no background processing.</summary>
|
||||||
|
/// <typeparam name="TKey">The cache key type.</typeparam>
|
||||||
|
/// <typeparam name="TValue">The cache value type.</typeparam>
|
||||||
|
/// <remarks>This is optimized for small caches that are reset relatively rarely. Each cache entry is marked as hot (accessed since the interval started) or stale.
|
||||||
|
/// When a new interval is started, stale entries are cleared and hot entries become stale.</remarks>
|
||||||
|
internal class IntervalMemoryCache<TKey, TValue>
|
||||||
|
where TKey : notnull
|
||||||
|
{
|
||||||
|
/*********
|
||||||
|
** Fields
|
||||||
|
*********/
|
||||||
|
/// <summary>The cached values that were accessed during the current interval.</summary>
|
||||||
|
private Dictionary<TKey, TValue> HotCache = new();
|
||||||
|
|
||||||
|
/// <summary>The cached values that will expire on the next interval.</summary>
|
||||||
|
private Dictionary<TKey, TValue> StaleCache = new();
|
||||||
|
|
||||||
|
|
||||||
|
/*********
|
||||||
|
** Public methods
|
||||||
|
*********/
|
||||||
|
/// <summary>Get a value from the cache, fetching it first if needed.</summary>
|
||||||
|
/// <param name="cacheKey">The unique key for the cached value.</param>
|
||||||
|
/// <param name="get">Get the latest data if it's not in the cache yet.</param>
|
||||||
|
public TValue GetOrSet(TKey cacheKey, Func<TValue> get)
|
||||||
|
{
|
||||||
|
// from hot cache
|
||||||
|
if (this.HotCache.TryGetValue(cacheKey, out TValue? value))
|
||||||
|
return value;
|
||||||
|
|
||||||
|
// from stale cache
|
||||||
|
if (this.StaleCache.TryGetValue(cacheKey, out value))
|
||||||
|
{
|
||||||
|
this.HotCache[cacheKey] = value;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// new value
|
||||||
|
value = get();
|
||||||
|
this.HotCache[cacheKey] = value;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Start a new cache interval, removing any stale entries.</summary>
|
||||||
|
public void StartNewInterval()
|
||||||
|
{
|
||||||
|
this.StaleCache.Clear();
|
||||||
|
if (this.HotCache.Count is not 0)
|
||||||
|
(this.StaleCache, this.HotCache) = (this.HotCache, this.StaleCache); // swap hot cache to stale
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -53,6 +53,9 @@ namespace StardewModdingAPI.Metadata
|
||||||
|
|
||||||
// detect Harmony & rewrite for SMAPI 3.12 (Harmony 1.x => 2.0 update)
|
// detect Harmony & rewrite for SMAPI 3.12 (Harmony 1.x => 2.0 update)
|
||||||
yield return new HarmonyRewriter();
|
yield return new HarmonyRewriter();
|
||||||
|
|
||||||
|
// detect issues for SMAPI 4.0.0
|
||||||
|
yield return new LegacyAssemblyFinder();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
yield return new HarmonyRewriter(shouldRewrite: false);
|
yield return new HarmonyRewriter(shouldRewrite: false);
|
||||||
|
|
|
@ -28,6 +28,8 @@
|
||||||
<PackageReference Include="Pintail" Version="2.1.0" />
|
<PackageReference Include="Pintail" Version="2.1.0" />
|
||||||
<PackageReference Include="Platonymous.TMXTile" Version="1.5.9" />
|
<PackageReference Include="Platonymous.TMXTile" Version="1.5.9" />
|
||||||
<PackageReference Include="System.Reflection.Emit" Version="4.7.0" />
|
<PackageReference Include="System.Reflection.Emit" Version="4.7.0" />
|
||||||
|
|
||||||
|
<!-- legacy package; remove in SMAPI 4.0.0 -->
|
||||||
<PackageReference Include="System.Runtime.Caching" Version="5.0.0" />
|
<PackageReference Include="System.Runtime.Caching" Version="5.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue