diff options
Diffstat (limited to 'src/SMAPI/Framework')
-rw-r--r-- | src/SMAPI/Framework/IModMetadata.cs | 4 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 38 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModLoading/Finders/LegacyAssemblyFinder.cs | 49 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModLoading/InstructionHandleResult.cs | 11 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModLoading/ModMetadata.cs | 7 | ||||
-rw-r--r-- | src/SMAPI/Framework/Reflection/CacheEntry.cs | 30 | ||||
-rw-r--r-- | src/SMAPI/Framework/Reflection/Reflector.cs | 110 | ||||
-rw-r--r-- | src/SMAPI/Framework/SCore.cs | 27 | ||||
-rw-r--r-- | src/SMAPI/Framework/Utilities/IntervalMemoryCache.cs | 57 |
9 files changed, 250 insertions, 83 deletions
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 + } + } +} |