From 9ef3f7edb1589a52794c7da7075996d4a02de6e7 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 30 May 2022 17:30:26 -0400 Subject: [PATCH 1/4] remove `System.Reflection.Metadata.MetadataUpdater.IsSupported: false` in runtime config This doesn't seem to be needed, and was probably added as part of the early experimenting with self-contained .NET. --- docs/release-notes.md | 4 ++++ src/SMAPI.Installer/assets/runtimeconfig.json | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 4770bd4f9..cdc0f5486 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,6 +1,10 @@ ← [README](README.md) # Release notes +## Upcoming release +* 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 34018b8a9..bd6a5240f 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 } } } From 9992915f565578949cad8d9bb8ceb360e0db5c85 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 31 May 2022 18:32:23 -0400 Subject: [PATCH 2/4] replace MemoryCache with custom cache This was causing significant frame stutters for some players since the migration to .NET 5 in Stardew Valley 1.5.5. --- build/common.targets | 3 - build/unix/prepare-install-package.sh | 4 - build/windows/prepare-install-package.ps1 | 4 - docs/release-notes.md | 2 + src/SMAPI/Framework/Reflection/CacheEntry.cs | 30 ----- src/SMAPI/Framework/Reflection/Reflector.cs | 110 +++++++++--------- src/SMAPI/Framework/SCore.cs | 2 + .../Utilities/IntervalMemoryCache.cs | 57 +++++++++ src/SMAPI/SMAPI.csproj | 1 - 9 files changed, 119 insertions(+), 94 deletions(-) delete mode 100644 src/SMAPI/Framework/Reflection/CacheEntry.cs create mode 100644 src/SMAPI/Framework/Utilities/IntervalMemoryCache.cs diff --git a/build/common.targets b/build/common.targets index 2e37e729f..b2441af8b 100644 --- a/build/common.targets +++ b/build/common.targets @@ -69,10 +69,7 @@ - - - diff --git a/build/unix/prepare-install-package.sh b/build/unix/prepare-install-package.sh index 9b195f37d..01c3a0ec6 100755 --- a/build/unix/prepare-install-package.sh +++ b/build/unix/prepare-install-package.sh @@ -142,15 +142,11 @@ 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 diff --git a/build/windows/prepare-install-package.ps1 b/build/windows/prepare-install-package.ps1 index 5e116019c..6731486b1 100644 --- a/build/windows/prepare-install-package.ps1 +++ b/build/windows/prepare-install-package.ps1 @@ -162,16 +162,12 @@ 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" } diff --git a/docs/release-notes.md b/docs/release-notes.md index cdc0f5486..31f6edc29 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,8 @@ # Release notes ## Upcoming release +* For players: + * Optimized reflection cache to reduce frame skips for some players. * For mod authors: * Removed `runtimeconfig.json` setting which impacted hot reload support. diff --git a/src/SMAPI/Framework/Reflection/CacheEntry.cs b/src/SMAPI/Framework/Reflection/CacheEntry.cs deleted file mode 100644 index 27f48a1f0..000000000 --- a/src/SMAPI/Framework/Reflection/CacheEntry.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Reflection; - -namespace StardewModdingAPI.Framework.Reflection -{ - /// A cached member reflection result. - internal readonly struct CacheEntry - { - /********* - ** Accessors - *********/ - /// Whether the lookup found a valid match. - [MemberNotNullWhen(true, nameof(CacheEntry.MemberInfo))] - public bool IsValid => this.MemberInfo != null; - - /// The reflection data for this member (or null if invalid). - public MemberInfo? MemberInfo { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The reflection data for this member (or null if invalid). - public CacheEntry(MemberInfo? memberInfo) - { - this.MemberInfo = memberInfo; - } - } -} diff --git a/src/SMAPI/Framework/Reflection/Reflector.cs b/src/SMAPI/Framework/Reflection/Reflector.cs index 79575c264..502a8519a 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 @@ internal class Reflector ** Fields *********/ /// The cached fields and methods found via reflection. - private readonly MemoryCache Cache = new(typeof(Reflector).FullName!); - - /// The sliding cache expiration time. - private readonly TimeSpan SlidingCacheExpiry = TimeSpan.FromMinutes(5); + private readonly IntervalMemoryCache Cache = new(); /********* @@ -136,6 +133,15 @@ public IReflectedMethod GetMethod(Type type, string name, bool required = true) return method!; } + /**** + ** Management + ****/ + /// Start a new cache interval, clearing stale reflection lookups. + public void NewCacheInterval() + { + this.Cache.StartNewInterval(); + } + /********* ** Private methods @@ -149,20 +155,23 @@ public IReflectedMethod GetMethod(Type type, string name, bool required = true) private IReflectedField? GetFieldFromHierarchy(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(type, obj, field, isStatic) @@ -178,20 +187,23 @@ public IReflectedMethod GetMethod(Type type, string name, bool required = true) private IReflectedProperty? GetPropertyFromHierarchy(Type type, object? obj, string name, BindingFlags bindingFlags) { bool isStatic = bindingFlags.HasFlag(BindingFlags.Static); - PropertyInfo? property = this.GetCached($"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(type, obj, property, isStatic) @@ -206,47 +218,41 @@ public IReflectedMethod GetMethod(Type type, string name, bool required = true) 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; } /// Get a method or field through the cache. /// The expected type. - /// The cache key. + /// A letter representing the member type (like 'm' for method). + /// The type whose members are being reflected. + /// The member name. + /// Whether the member is static. /// Fetches a new value to cache. - private TMemberInfo? GetCached(string key, Func fetch) + private TMemberInfo? GetCached(char memberType, Type type, string memberName, bool isStatic, Func 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 67f784001..c453562ff 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -1164,6 +1164,8 @@ internal void OnLoadStageChanged(LoadStage newStage) protected void OnNewDayAfterFade() { this.EventManager.DayEnding.RaiseEmpty(); + + this.Reflection.NewCacheInterval(); } /// A callback invoked after an asset is fully loaded through a content manager. diff --git a/src/SMAPI/Framework/Utilities/IntervalMemoryCache.cs b/src/SMAPI/Framework/Utilities/IntervalMemoryCache.cs new file mode 100644 index 000000000..d2b69f518 --- /dev/null +++ b/src/SMAPI/Framework/Utilities/IntervalMemoryCache.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; + +namespace StardewModdingAPI.Framework.Utilities +{ + /// A memory cache with sliding expiry based on custom intervals, with no background processing. + /// The cache key type. + /// The cache value type. + /// 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. + internal class IntervalMemoryCache + where TKey : notnull + { + /********* + ** Fields + *********/ + /// The cached values that were accessed during the current interval. + private Dictionary HotCache = new(); + + /// The cached values that will expire on the next interval. + private Dictionary StaleCache = new(); + + + /********* + ** Public methods + *********/ + /// Get a value from the cache, fetching it first if needed. + /// The unique key for the cached value. + /// Get the latest data if it's not in the cache yet. + public TValue GetOrSet(TKey cacheKey, Func 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; + } + + /// Start a new cache interval, removing any stale entries. + 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/SMAPI.csproj b/src/SMAPI/SMAPI.csproj index 270446794..95249bfd1 100644 --- a/src/SMAPI/SMAPI.csproj +++ b/src/SMAPI/SMAPI.csproj @@ -28,7 +28,6 @@ - From bf960ce283d794a11885a5fde6f123a4e6827853 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 31 May 2022 21:23:44 -0400 Subject: [PATCH 3/4] add backwards compatibility for mods using now-unused dependencies --- build/common.targets | 5 ++ build/unix/prepare-install-package.sh | 5 ++ build/windows/prepare-install-package.ps1 | 5 ++ .../Framework/ModData/ModWarning.cs | 11 ++++- src/SMAPI/Framework/IModMetadata.cs | 4 ++ .../Framework/ModLoading/AssemblyLoader.cs | 38 ++++++++++++++ .../Finders/LegacyAssemblyFinder.cs | 49 +++++++++++++++++++ .../ModLoading/InstructionHandleResult.cs | 11 ++++- src/SMAPI/Framework/ModLoading/ModMetadata.cs | 7 +++ src/SMAPI/Framework/SCore.cs | 25 ++++++++++ src/SMAPI/Metadata/InstructionMetadata.cs | 3 ++ src/SMAPI/SMAPI.csproj | 3 ++ 12 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 src/SMAPI/Framework/ModLoading/Finders/LegacyAssemblyFinder.cs diff --git a/build/common.targets b/build/common.targets index b2441af8b..10b94d7e3 100644 --- a/build/common.targets +++ b/build/common.targets @@ -70,6 +70,11 @@ + + + + + diff --git a/build/unix/prepare-install-package.sh b/build/unix/prepare-install-package.sh index 01c3a0ec6..01cd20808 100755 --- a/build/unix/prepare-install-package.sh +++ b/build/unix/prepare-install-package.sh @@ -151,6 +151,11 @@ for folder in ${folders[@]}; do 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 6731486b1..7e3c6c865 100644 --- a/build/windows/prepare-install-package.ps1 +++ b/build/windows/prepare-install-package.ps1 @@ -172,6 +172,11 @@ foreach ($folder in $folders) { 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/src/SMAPI.Toolkit/Framework/ModData/ModWarning.cs b/src/SMAPI.Toolkit/Framework/ModData/ModWarning.cs index cf804df46..32c2ed6d2 100644 --- a/src/SMAPI.Toolkit/Framework/ModData/ModWarning.cs +++ b/src/SMAPI.Toolkit/Framework/ModData/ModWarning.cs @@ -35,6 +35,15 @@ public enum ModWarning AccessesFilesystem = 128, /// Uses .NET APIs for shell or process access. - AccessesShell = 256 + AccessesShell = 256, + + /// References the legacy System.Configuration.ConfigurationManager assembly and doesn't include a copy in the mod folder, so it'll break in SMAPI 4.0.0. + DetectedLegacyConfigurationDll = 512, + + /// References the legacy System.Runtime.Caching assembly and doesn't include a copy in the mod folder, so it'll break in SMAPI 4.0.0. + DetectedLegacyCachingDll = 1024, + + /// References the legacy System.Security.Permissions assembly and doesn't include a copy in the mod folder, so it'll break in SMAPI 4.0.0. + DetectedLegacyPermissionsDll = 2048 } } diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs index 7cee20b9e..be25c070f 100644 --- a/src/SMAPI/Framework/IModMetadata.cs +++ b/src/SMAPI/Framework/IModMetadata.cs @@ -88,6 +88,10 @@ internal interface IModMetadata : IModInfo /// The warning to set. IModMetadata SetWarning(ModWarning warning); + /// Remove a warning flag for the mod. + /// The warning to remove. + IModMetadata RemoveWarning(ModWarning warning); + /// Set the mod instance. /// The mod instance to set. /// The translations for this mod (if loaded). diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index fb5ebc013..e5aaa8ee0 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -163,6 +163,29 @@ select name 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 @@ private void ProcessInstructionHandleResult(IModMetadata mod, IInstructionHandle 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 000000000..d3437b05e --- /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 +{ + /// Detects assembly references which will break in SMAPI 4.0.0. + internal class LegacyAssemblyFinder : BaseInstructionHandler + { + /********* + ** Public methods + *********/ + /// Construct an instance. + public LegacyAssemblyFinder() + : base(defaultPhrase: "legacy assembly references") { } + + + /// + 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 + *********/ + /// Get the instruction handle flag for the given assembly reference, if any. + /// The assembly reference. + 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 e3f108cb8..476c30d02 100644 --- a/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs +++ b/src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs @@ -30,6 +30,15 @@ internal enum InstructionHandleResult DetectedFilesystemAccess, /// The instruction accesses the OS shell or processes directly. - DetectedShellAccess + DetectedShellAccess, + + /// The module references the legacy System.Configuration.ConfigurationManager assembly and doesn't include a copy in the mod folder, so it'll break in SMAPI 4.0.0. + DetectedLegacyConfigurationDll, + + /// The module references the legacy System.Runtime.Caching assembly and doesn't include a copy in the mod folder, so it'll break in SMAPI 4.0.0. + DetectedLegacyCachingDll, + + /// The module references the legacy System.Security.Permissions assembly and doesn't include a copy in the mod folder, so it'll break in SMAPI 4.0.0. + DetectedLegacyPermissionsDll } } diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index fe54634b3..aa4d2d8ce 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -138,6 +138,13 @@ public IModMetadata SetWarning(ModWarning warning) return this; } + /// + public IModMetadata RemoveWarning(ModWarning warning) + { + this.ActualWarnings &= ~warning; + return this; + } + /// public IModMetadata SetMod(IMod mod, TranslationHelper translations) { diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index c453562ff..731731d46 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -1679,6 +1679,31 @@ private void LoadMods(IModMetadata[] mods, JsonHelper jsonHelper, ContentCoordin } #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/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index 4d512546d..dce0c6b1e 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -53,6 +53,9 @@ public IEnumerable GetHandlers(bool paranoidMode, bool plat // 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 95249bfd1..a0ca54cca 100644 --- a/src/SMAPI/SMAPI.csproj +++ b/src/SMAPI/SMAPI.csproj @@ -28,6 +28,9 @@ + + + From 1cded44f7a5626a0d4f5c3ce48a83aa544d517c3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 1 Jun 2022 19:58:35 -0400 Subject: [PATCH 4/4] prepare for release --- build/common.targets | 2 +- docs/release-notes.md | 4 +++- src/SMAPI.Mods.ConsoleCommands/manifest.json | 4 ++-- src/SMAPI.Mods.ErrorHandler/manifest.json | 4 ++-- src/SMAPI.Mods.SaveBackup/manifest.json | 4 ++-- src/SMAPI/Constants.cs | 2 +- 6 files changed, 11 insertions(+), 9 deletions(-) diff --git a/build/common.targets b/build/common.targets index 10b94d7e3..ca9a1d121 100644 --- a/build/common.targets +++ b/build/common.targets @@ -1,7 +1,7 @@ - 3.14.6 + 3.14.7 SMAPI latest $(AssemblySearchPaths);{GAC} diff --git a/docs/release-notes.md b/docs/release-notes.md index 31f6edc29..d8cfa350d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,7 +1,9 @@ ← [README](README.md) # Release notes -## Upcoming release +## 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: diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index 9d9a80613..564e480e7 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 07c2512bf..39d22b5f3 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 ec048deaf..8eaf24750 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/Constants.cs b/src/SMAPI/Constants.cs index 9212fc901..c63324e34 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -50,7 +50,7 @@ internal static class EarlyConstants internal static int? LogScreenId { get; set; } /// SMAPI's current raw semantic version. - internal static string RawApiVersion = "3.14.6"; + internal static string RawApiVersion = "3.14.7"; } /// Contains SMAPI's constants and assumptions.