diff options
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> |