summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--build/common.targets6
-rwxr-xr-xbuild/unix/prepare-install-package.sh9
-rw-r--r--build/windows/prepare-install-package.ps19
-rw-r--r--docs/release-notes.md8
-rw-r--r--src/SMAPI.Installer/assets/runtimeconfig.json5
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/manifest.json4
-rw-r--r--src/SMAPI.Mods.ErrorHandler/manifest.json4
-rw-r--r--src/SMAPI.Mods.SaveBackup/manifest.json4
-rw-r--r--src/SMAPI.Toolkit/Framework/ModData/ModWarning.cs11
-rw-r--r--src/SMAPI/Constants.cs2
-rw-r--r--src/SMAPI/Framework/IModMetadata.cs4
-rw-r--r--src/SMAPI/Framework/ModLoading/AssemblyLoader.cs38
-rw-r--r--src/SMAPI/Framework/ModLoading/Finders/LegacyAssemblyFinder.cs49
-rw-r--r--src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs11
-rw-r--r--src/SMAPI/Framework/ModLoading/ModMetadata.cs7
-rw-r--r--src/SMAPI/Framework/Reflection/CacheEntry.cs30
-rw-r--r--src/SMAPI/Framework/Reflection/Reflector.cs110
-rw-r--r--src/SMAPI/Framework/SCore.cs27
-rw-r--r--src/SMAPI/Framework/Utilities/IntervalMemoryCache.cs57
-rw-r--r--src/SMAPI/Metadata/InstructionMetadata.cs3
-rw-r--r--src/SMAPI/SMAPI.csproj2
21 files changed, 297 insertions, 103 deletions
diff --git a/build/common.targets b/build/common.targets
index 2e37e729..ca9a1d12 100644
--- a/build/common.targets
+++ b/build/common.targets
@@ -1,7 +1,7 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<!--set general build properties -->
- <Version>3.14.6</Version>
+ <Version>3.14.7</Version>
<Product>SMAPI</Product>
<LangVersion>latest</LangVersion>
<AssemblySearchPaths>$(AssemblySearchPaths);{GAC}</AssemblySearchPaths>
@@ -69,8 +69,10 @@
<Copy SourceFiles="$(TargetDir)\MonoMod.Common.dll" DestinationFolder="$(GamePath)\smapi-internal" />
<!-- .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'" />
+
+ <!-- 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.Security.Permissions.dll" DestinationFolder="$(GamePath)\smapi-internal" />
</Target>
diff --git a/build/unix/prepare-install-package.sh b/build/unix/prepare-install-package.sh
index 9b195f37..01cd2080 100755
--- a/build/unix/prepare-install-package.sh
+++ b/build/unix/prepare-install-package.sh
@@ -142,19 +142,20 @@ for folder in ${folders[@]}; do
cp "$smapiBin/SMAPI.metadata.json" "$bundlePath/smapi-internal/metadata.json"
if [ $folder == "linux" ] || [ $folder == "macOS" ]; then
cp "$installAssets/unix-launcher.sh" "$bundlePath"
- cp "$smapiBin/System.Runtime.Caching.dll" "$bundlePath/smapi-internal"
else
cp "$installAssets/windows-exe-config.xml" "$bundlePath/StardewModdingAPI.exe.config"
fi
# 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
cp "$smapiBin/System.Management.dll" "$bundlePath/smapi-internal"
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
for modName in ${bundleModNames[@]}; do
fromPath="src/SMAPI.Mods.$modName/bin/$buildConfig/$runtime/publish"
diff --git a/build/windows/prepare-install-package.ps1 b/build/windows/prepare-install-package.ps1
index 5e116019..7e3c6c86 100644
--- a/build/windows/prepare-install-package.ps1
+++ b/build/windows/prepare-install-package.ps1
@@ -162,20 +162,21 @@ foreach ($folder in $folders) {
cp "$smapiBin/SMAPI.metadata.json" "$bundlePath/smapi-internal/metadata.json"
if ($folder -eq "linux" -or $folder -eq "macOS") {
cp "$installAssets/unix-launcher.sh" "$bundlePath"
- cp "$smapiBin/System.Runtime.Caching.dll" "$bundlePath/smapi-internal"
}
else {
cp "$installAssets/windows-exe-config.xml" "$bundlePath/StardewModdingAPI.exe.config"
}
# 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") {
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
foreach ($modName in $bundleModNames) {
$fromPath = "src/SMAPI.Mods.$modName/bin/$buildConfig/$runtime/publish"
diff --git a/docs/release-notes.md b/docs/release-notes.md
index 4770bd4f..d8cfa350 100644
--- a/docs/release-notes.md
+++ b/docs/release-notes.md
@@ -1,6 +1,14 @@
← [README](README.md)
# 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
Released 27 May 2022 for Stardew Valley 1.5.6 or later.
diff --git a/src/SMAPI.Installer/assets/runtimeconfig.json b/src/SMAPI.Installer/assets/runtimeconfig.json
index 34018b8a..bd6a5240 100644
--- a/src/SMAPI.Installer/assets/runtimeconfig.json
+++ b/src/SMAPI.Installer/assets/runtimeconfig.json
@@ -9,8 +9,9 @@
}
],
"configProperties": {
- "System.Runtime.TieredCompilation": false,
- "System.Reflection.Metadata.MetadataUpdater.IsSupported": false
+ // disable tiered runtime JIT: https://github.com/dotnet/runtime/blob/main/docs/design/features/tiered-compilation.md
+ // This is disabled by the base game, and causes issues with Harmony patches.
+ "System.Runtime.TieredCompilation": false
}
}
}
diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json
index 9d9a8061..564e480e 100644
--- a/src/SMAPI.Mods.ConsoleCommands/manifest.json
+++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json
@@ -1,9 +1,9 @@
{
"Name": "Console Commands",
"Author": "SMAPI",
- "Version": "3.14.6",
+ "Version": "3.14.7",
"Description": "Adds SMAPI console commands that let you manipulate the game.",
"UniqueID": "SMAPI.ConsoleCommands",
"EntryDll": "ConsoleCommands.dll",
- "MinimumApiVersion": "3.14.6"
+ "MinimumApiVersion": "3.14.7"
}
diff --git a/src/SMAPI.Mods.ErrorHandler/manifest.json b/src/SMAPI.Mods.ErrorHandler/manifest.json
index 07c2512b..39d22b5f 100644
--- a/src/SMAPI.Mods.ErrorHandler/manifest.json
+++ b/src/SMAPI.Mods.ErrorHandler/manifest.json
@@ -1,9 +1,9 @@
{
"Name": "Error Handler",
"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.",
"UniqueID": "SMAPI.ErrorHandler",
"EntryDll": "ErrorHandler.dll",
- "MinimumApiVersion": "3.14.6"
+ "MinimumApiVersion": "3.14.7"
}
diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json
index ec048dea..8eaf2475 100644
--- a/src/SMAPI.Mods.SaveBackup/manifest.json
+++ b/src/SMAPI.Mods.SaveBackup/manifest.json
@@ -1,9 +1,9 @@
{
"Name": "Save Backup",
"Author": "SMAPI",
- "Version": "3.14.6",
+ "Version": "3.14.7",
"Description": "Automatically backs up all your saves once per day into its folder.",
"UniqueID": "SMAPI.SaveBackup",
"EntryDll": "SaveBackup.dll",
- "MinimumApiVersion": "3.14.6"
+ "MinimumApiVersion": "3.14.7"
}
diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModWarning.cs b/src/SMAPI.Toolkit/Framework/ModData/ModWarning.cs
index cf804df4..32c2ed6d 100644
--- a/src/SMAPI.Toolkit/Framework/ModData/ModWarning.cs
+++ b/src/SMAPI.Toolkit/Framework/ModData/ModWarning.cs
@@ -35,6 +35,15 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
AccessesFilesystem = 128,
/// <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
}
}
diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs
index 9212fc90..c63324e3 100644
--- a/src/SMAPI/Constants.cs
+++ b/src/SMAPI/Constants.cs
@@ -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.6";
+ internal static string RawApiVersion = "3.14.7";
}
/// <summary>Contains SMAPI's constants and assumptions.</summary>
diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs
index 7cee20b9..be25c070 100644
--- a/src/SMAPI/Framework/IModMetadata.cs
+++ b/src/SMAPI/Framework/IModMetadata.cs
@@ -88,6 +88,10 @@ namespace StardewModdingAPI.Framework
/// <param name="warning">The warning to set.</param>
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>
/// <param name="mod">The mod instance to set.</param>
/// <param name="translations">The translations for this mod (if loaded).</param>
diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs
index fb5ebc01..e5aaa8ee 100644
--- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs
+++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs
@@ -163,6 +163,29 @@ namespace StardewModdingAPI.Framework.ModLoading
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
if (!assumeCompatible && mod.Warnings.HasFlag(ModWarning.BrokenCodeLoaded))
throw new IncompatibleInstructionException();
@@ -429,6 +452,21 @@ namespace StardewModdingAPI.Framework.ModLoading
mod.SetWarning(ModWarning.AccessesShell);
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:
break;
diff --git a/src/SMAPI/Framework/ModLoading/Finders/LegacyAssemblyFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/LegacyAssemblyFinder.cs
new file mode 100644
index 00000000..d3437b05
--- /dev/null
+++ b/src/SMAPI/Framework/ModLoading/Finders/LegacyAssemblyFinder.cs
@@ -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
+ };
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs b/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs
index e3f108cb..476c30d0 100644
--- a/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs
+++ b/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs
@@ -30,6 +30,15 @@ namespace StardewModdingAPI.Framework.ModLoading
DetectedFilesystemAccess,
/// <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
}
}
diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs
index fe54634b..aa4d2d8c 100644
--- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs
+++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs
@@ -139,6 +139,13 @@ namespace StardewModdingAPI.Framework.ModLoading
}
/// <inheritdoc />
+ public IModMetadata RemoveWarning(ModWarning warning)
+ {
+ this.ActualWarnings &= ~warning;
+ return this;
+ }
+
+ /// <inheritdoc />
public IModMetadata SetMod(IMod mod, TranslationHelper translations)
{
if (this.ContentPack != null)
diff --git a/src/SMAPI/Framework/Reflection/CacheEntry.cs b/src/SMAPI/Framework/Reflection/CacheEntry.cs
deleted file mode 100644
index 27f48a1f..00000000
--- a/src/SMAPI/Framework/Reflection/CacheEntry.cs
+++ /dev/null
@@ -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;
- }
- }
-}
diff --git a/src/SMAPI/Framework/Reflection/Reflector.cs b/src/SMAPI/Framework/Reflection/Reflector.cs
index 79575c26..502a8519 100644
--- a/src/SMAPI/Framework/Reflection/Reflector.cs
+++ b/src/SMAPI/Framework/Reflection/Reflector.cs
@@ -1,6 +1,6 @@
using System;
using System.Reflection;
-using System.Runtime.Caching;
+using StardewModdingAPI.Framework.Utilities;
namespace StardewModdingAPI.Framework.Reflection
{
@@ -12,10 +12,7 @@ namespace StardewModdingAPI.Framework.Reflection
** Fields
*********/
/// <summary>The cached fields and methods found via reflection.</summary>
- private readonly MemoryCache Cache = new(typeof(Reflector).FullName!);
-
- /// <summary>The sliding cache expiration time.</summary>
- private readonly TimeSpan SlidingCacheExpiry = TimeSpan.FromMinutes(5);
+ private readonly IntervalMemoryCache<string, MemberInfo?> Cache = new();
/*********
@@ -136,6 +133,15 @@ namespace StardewModdingAPI.Framework.Reflection
return method!;
}
+ /****
+ ** Management
+ ****/
+ /// <summary>Start a new cache interval, clearing stale reflection lookups.</summary>
+ public void NewCacheInterval()
+ {
+ this.Cache.StartNewInterval();
+ }
+
/*********
** Private methods
@@ -149,20 +155,23 @@ namespace StardewModdingAPI.Framework.Reflection
private IReflectedField<TValue>? GetFieldFromHierarchy<TValue>(Type type, object? obj, string name, BindingFlags bindingFlags)
{
bool isStatic = bindingFlags.HasFlag(BindingFlags.Static);
- FieldInfo? field = this.GetCached($"field::{isStatic}::{type.FullName}::{name}", () =>
- {
- for (Type? curType = type; curType != null; curType = curType.BaseType)
+ FieldInfo? field = this.GetCached(
+ 'f', type, name, isStatic,
+ fetch: () =>
{
- FieldInfo? fieldInfo = curType.GetField(name, bindingFlags);
- if (fieldInfo != null)
+ for (Type? curType = type; curType != null; curType = curType.BaseType)
{
- type = curType;
- return fieldInfo;
+ FieldInfo? fieldInfo = curType.GetField(name, bindingFlags);
+ if (fieldInfo != null)
+ {
+ type = curType;
+ return fieldInfo;
+ }
}
- }
- return null;
- });
+ return null;
+ }
+ );
return field != null
? 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)
{
bool isStatic = bindingFlags.HasFlag(BindingFlags.Static);
- PropertyInfo? property = this.GetCached<PropertyInfo>($"property::{isStatic}::{type.FullName}::{name}", () =>
- {
- for (Type? curType = type; curType != null; curType = curType.BaseType)
+ PropertyInfo? property = this.GetCached(
+ 'p', type, name, isStatic,
+ fetch: () =>
{
- PropertyInfo? propertyInfo = curType.GetProperty(name, bindingFlags);
- if (propertyInfo != null)
+ for (Type? curType = type; curType != null; curType = curType.BaseType)
{
- type = curType;
- return propertyInfo;
+ PropertyInfo? propertyInfo = curType.GetProperty(name, bindingFlags);
+ if (propertyInfo != null)
+ {
+ type = curType;
+ return propertyInfo;
+ }
}
- }
- return null;
- });
+ return null;
+ }
+ );
return property != null
? 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)
{
bool isStatic = bindingFlags.HasFlag(BindingFlags.Static);
- MethodInfo? method = this.GetCached($"method::{isStatic}::{type.FullName}::{name}", () =>
- {
- for (Type? curType = type; curType != null; curType = curType.BaseType)
+ MethodInfo? method = this.GetCached(
+ 'm', type, name, isStatic,
+ fetch: () =>
{
- MethodInfo? methodInfo = curType.GetMethod(name, bindingFlags);
- if (methodInfo != null)
+ for (Type? curType = type; curType != null; curType = curType.BaseType)
{
- type = curType;
- return methodInfo;
+ MethodInfo? methodInfo = curType.GetMethod(name, bindingFlags);
+ if (methodInfo != null)
+ {
+ type = curType;
+ return methodInfo;
+ }
}
- }
- return null;
- });
+ return null;
+ }
+ );
return method != null
- ? new ReflectedMethod(type, obj, method, isStatic: bindingFlags.HasFlag(BindingFlags.Static))
+ ? new ReflectedMethod(type, obj, method, isStatic: isStatic)
: null;
}
/// <summary>Get a method or field through the cache.</summary>
/// <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>
- 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
{
- // get from cache
- if (this.Cache.Contains(key))
- {
- 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;
+ string key = $"{memberType}{(isStatic ? 's' : 'i')}{type.FullName}:{memberName}";
+ return (TMemberInfo?)this.Cache.GetOrSet(key, fetch);
}
}
}
diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs
index 67f78400..731731d4 100644
--- a/src/SMAPI/Framework/SCore.cs
+++ b/src/SMAPI/Framework/SCore.cs
@@ -1164,6 +1164,8 @@ namespace StardewModdingAPI.Framework
protected void OnNewDayAfterFade()
{
this.EventManager.DayEnding.RaiseEmpty();
+
+ this.Reflection.NewCacheInterval();
}
/// <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
+ // 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
Context.HeuristicModsRunningCode.Push(metadata);
try
diff --git a/src/SMAPI/Framework/Utilities/IntervalMemoryCache.cs b/src/SMAPI/Framework/Utilities/IntervalMemoryCache.cs
new file mode 100644
index 00000000..d2b69f51
--- /dev/null
+++ b/src/SMAPI/Framework/Utilities/IntervalMemoryCache.cs
@@ -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
+ }
+ }
+}
diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs
index 4d512546..dce0c6b1 100644
--- a/src/SMAPI/Metadata/InstructionMetadata.cs
+++ b/src/SMAPI/Metadata/InstructionMetadata.cs
@@ -53,6 +53,9 @@ namespace StardewModdingAPI.Metadata
// detect Harmony & rewrite for SMAPI 3.12 (Harmony 1.x => 2.0 update)
yield return new HarmonyRewriter();
+
+ // detect issues for SMAPI 4.0.0
+ yield return new LegacyAssemblyFinder();
}
else
yield return new HarmonyRewriter(shouldRewrite: false);
diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj
index 27044679..a0ca54cc 100644
--- a/src/SMAPI/SMAPI.csproj
+++ b/src/SMAPI/SMAPI.csproj
@@ -28,6 +28,8 @@
<PackageReference Include="Pintail" Version="2.1.0" />
<PackageReference Include="Platonymous.TMXTile" Version="1.5.9" />
<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" />
</ItemGroup>