Merge branch 'develop' into stable

This commit is contained in:
Jesse Plamondon-Willard 2022-06-01 19:59:00 -04:00
commit e10147e7bd
No known key found for this signature in database
GPG Key ID: CF8B1456B3E29F49
21 changed files with 297 additions and 103 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.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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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.6"; internal static string RawApiVersion = "3.14.7";
} }
/// <summary>Contains SMAPI's constants and assumptions.</summary> /// <summary>Contains SMAPI's constants and assumptions.</summary>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,20 +155,23 @@ 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,
for (Type? curType = type; curType != null; curType = curType.BaseType) fetch: () =>
{ {
FieldInfo? fieldInfo = curType.GetField(name, bindingFlags); for (Type? curType = type; curType != null; curType = curType.BaseType)
if (fieldInfo != null)
{ {
type = curType; FieldInfo? fieldInfo = curType.GetField(name, bindingFlags);
return fieldInfo; if (fieldInfo != null)
{
type = curType;
return fieldInfo;
}
} }
}
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,20 +187,23 @@ 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,
for (Type? curType = type; curType != null; curType = curType.BaseType) fetch: () =>
{ {
PropertyInfo? propertyInfo = curType.GetProperty(name, bindingFlags); for (Type? curType = type; curType != null; curType = curType.BaseType)
if (propertyInfo != null)
{ {
type = curType; PropertyInfo? propertyInfo = curType.GetProperty(name, bindingFlags);
return propertyInfo; if (propertyInfo != null)
{
type = curType;
return propertyInfo;
}
} }
}
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,47 +218,41 @@ 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,
for (Type? curType = type; curType != null; curType = curType.BaseType) fetch: () =>
{ {
MethodInfo? methodInfo = curType.GetMethod(name, bindingFlags); for (Type? curType = type; curType != null; curType = curType.BaseType)
if (methodInfo != null)
{ {
type = curType; MethodInfo? methodInfo = curType.GetMethod(name, bindingFlags);
return methodInfo; if (methodInfo != null)
{
type = curType;
return methodInfo;
}
} }
}
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;
} }
} }
} }

View File

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

View File

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

View File

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

View File

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