diff --git a/docs/release-notes.md b/docs/release-notes.md index 5bd81a0ba..8cbc4cd16 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ ## Upcoming release for Stardew Valley 1.6 * For players: * Updated for Stardew Valley 1.6. + * Added support for overriding SMAPI configuration per `Mods` folder (thanks to Shockah!). * Improved performance. * Improved compatibility rewriting to handle more cases (thanks to SinZ!). * Removed the bundled `ErrorHandler` mod (now integrated into Stardew Valley 1.6). @@ -14,16 +15,11 @@ * For mod authors: * Updated to .NET 6. * Added `RenderingStep` and `RenderedStep` events, which let you handle a specific step in the game's render cycle. + * Added support for [custom update manifests](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Update_checks#Custom_update_manifest) (thanks to Jamie Taylor!). * Removed all deprecated APIs. * SMAPI no longer intercepts output written to the console. Mods which directly access `Console` will be listed under mod warnings. * Calling `Monitor.VerboseLog` with an interpolated string no longer evaluates the string if verbose mode is disabled (thanks to atravita!). This only applies to mods compiled in SMAPI 4.0.0 or later. - -## Upcoming release -* For players: - * Added support for overriding SMAPI configuration per `Mods` folder (thanks to Shockah!). - -* For mod authors: - * Added support for [custom update manifests](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Update_checks#Custom_update_manifest) (thanks to Jamie Taylor!). + * Fixed redundant `TRACE` logs for a broken mod which references members with the wrong types. * For the web UI: * Fixed uploaded log/JSON file expiry alway shown as renewed. diff --git a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToInvalidMemberFinder.cs similarity index 58% rename from src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs rename to src/SMAPI/Framework/ModLoading/Finders/ReferenceToInvalidMemberFinder.cs index f34542c30..e5625c637 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToInvalidMemberFinder.cs @@ -7,9 +7,9 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders { - /// Finds references to a field, property, or method which returns a different type than the code expects. + /// Finds references to a field, property, or method which either doesn't exist or returns a different type than the code expects. /// This implementation is purely heuristic. It should never return a false positive, but won't detect all cases. - internal class ReferenceToMemberWithUnexpectedTypeFinder : BaseInstructionHandler + internal class ReferenceToInvalidMemberFinder : BaseInstructionHandler { /********* ** Fields @@ -23,7 +23,7 @@ internal class ReferenceToMemberWithUnexpectedTypeFinder : BaseInstructionHandle *********/ /// Construct an instance. /// The assembly names to which to heuristically detect broken references. - public ReferenceToMemberWithUnexpectedTypeFinder(ISet validateReferencesToAssemblies) + public ReferenceToInvalidMemberFinder(ISet validateReferencesToAssemblies) : base(defaultPhrase: "") { this.ValidateReferencesToAssemblies = validateReferencesToAssemblies; @@ -36,37 +36,45 @@ public override bool Handle(ModuleDefinition module, ILProcessor cil, Instructio FieldReference? fieldRef = RewriteHelper.AsFieldReference(instruction); if (fieldRef != null && this.ShouldValidate(fieldRef.DeclaringType)) { - // get target field FieldDefinition? targetField = fieldRef.DeclaringType.Resolve()?.Fields.FirstOrDefault(p => p.Name == fieldRef.Name); - if (targetField == null) - return false; - // validate return type - if (!RewriteHelper.LooksLikeSameType(fieldRef.FieldType, targetField.FieldType)) - { + // wrong return type + if (targetField != null && !RewriteHelper.LooksLikeSameType(fieldRef.FieldType, targetField.FieldType)) this.MarkFlag(InstructionHandleResult.NotCompatible, $"reference to {fieldRef.DeclaringType.FullName}.{fieldRef.Name} (field returns {this.GetFriendlyTypeName(targetField.FieldType)}, not {this.GetFriendlyTypeName(fieldRef.FieldType)})"); - return false; - } + + // missing + else if (targetField == null || targetField.HasConstant || !RewriteHelper.HasSameNamespaceAndName(fieldRef.DeclaringType, targetField.DeclaringType)) + this.MarkFlag(InstructionHandleResult.NotCompatible, $"reference to {fieldRef.DeclaringType.FullName}.{fieldRef.Name} (no such field)"); + + return false; } // method reference - MethodReference? methodReference = RewriteHelper.AsMethodReference(instruction); - if (methodReference != null && !this.IsUnsupported(methodReference) && this.ShouldValidate(methodReference.DeclaringType)) + MethodReference? methodRef = RewriteHelper.AsMethodReference(instruction); + if (methodRef != null && !this.IsUnsupported(methodRef)) { - // get potential targets - MethodDefinition[]? candidateMethods = methodReference.DeclaringType.Resolve()?.Methods.Where(found => found.Name == methodReference.Name).ToArray(); - if (candidateMethods == null || !candidateMethods.Any()) - return false; + MethodDefinition? methodDef = methodRef.Resolve(); - // compare return types - MethodDefinition? methodDef = methodReference.Resolve(); - if (methodDef == null) - return false; // validated by ReferenceToMissingMemberFinder + // wrong return type + if (methodDef != null && this.ShouldValidate(methodRef.DeclaringType)) + { + MethodDefinition[]? candidateMethods = methodRef.DeclaringType.Resolve()?.Methods.Where(found => found.Name == methodRef.Name).ToArray(); + if (candidateMethods?.Any() is true && candidateMethods.All(method => !RewriteHelper.LooksLikeSameType(method.ReturnType, methodDef.ReturnType))) + this.MarkFlag(InstructionHandleResult.NotCompatible, $"reference to {methodDef.DeclaringType.FullName}.{methodDef.Name} (no such method returns {this.GetFriendlyTypeName(methodDef.ReturnType)})"); + } - if (candidateMethods.All(method => !RewriteHelper.LooksLikeSameType(method.ReturnType, methodDef.ReturnType))) + // missing + else if (methodDef is null) { - this.MarkFlag(InstructionHandleResult.NotCompatible, $"reference to {methodDef.DeclaringType.FullName}.{methodDef.Name} (no such method returns {this.GetFriendlyTypeName(methodDef.ReturnType)})"); - return false; + string phrase; + if (this.IsProperty(methodRef)) + phrase = $"reference to {methodRef.DeclaringType.FullName}.{methodRef.Name.Substring(4)} (no such property)"; + else if (methodRef.Name == ".ctor") + phrase = $"reference to {methodRef.DeclaringType.FullName}.{methodRef.Name} (no matching constructor)"; + else + phrase = $"reference to {methodRef.DeclaringType.FullName}.{methodRef.Name} (no such method)"; + + this.MarkFlag(InstructionHandleResult.NotCompatible, phrase); } } @@ -116,5 +124,12 @@ private string GetFriendlyTypeName(TypeReference type) return type.FullName; } + + /// Get whether a method reference is a property getter or setter. + /// The method reference. + private bool IsProperty(MethodReference method) + { + return method.Name.StartsWith("get_") || method.Name.StartsWith("set_"); + } } } diff --git a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs deleted file mode 100644 index b54d57c4b..000000000 --- a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using Mono.Cecil; -using Mono.Cecil.Cil; -using StardewModdingAPI.Framework.ModLoading.Framework; - -namespace StardewModdingAPI.Framework.ModLoading.Finders -{ - /// Finds references to a field, property, or method which no longer exists. - /// This implementation is purely heuristic. It should never return a false positive, but won't detect all cases. - internal class ReferenceToMissingMemberFinder : BaseInstructionHandler - { - /********* - ** Fields - *********/ - /// The assembly names to which to heuristically detect broken references. - private readonly ISet ValidateReferencesToAssemblies; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The assembly names to which to heuristically detect broken references. - public ReferenceToMissingMemberFinder(ISet validateReferencesToAssemblies) - : base(defaultPhrase: "") - { - this.ValidateReferencesToAssemblies = validateReferencesToAssemblies; - } - - /// - public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction) - { - // field reference - FieldReference? fieldRef = RewriteHelper.AsFieldReference(instruction); - if (fieldRef != null && this.ShouldValidate(fieldRef.DeclaringType)) - { - FieldDefinition? target = fieldRef.Resolve(); - if (target == null || target.HasConstant || !RewriteHelper.HasSameNamespaceAndName(fieldRef.DeclaringType, target.DeclaringType)) - { - this.MarkFlag(InstructionHandleResult.NotCompatible, $"reference to {fieldRef.DeclaringType.FullName}.{fieldRef.Name} (no such field)"); - return false; - } - } - - // method reference - MethodReference? methodRef = RewriteHelper.AsMethodReference(instruction); - if (methodRef != null && this.ShouldValidate(methodRef.DeclaringType) && !this.IsUnsupported(methodRef)) - { - MethodDefinition? target = methodRef.Resolve(); - if (target == null) - { - string phrase; - if (this.IsProperty(methodRef)) - phrase = $"reference to {methodRef.DeclaringType.FullName}.{methodRef.Name.Substring(4)} (no such property)"; - else if (methodRef.Name == ".ctor") - phrase = $"reference to {methodRef.DeclaringType.FullName}.{methodRef.Name} (no matching constructor)"; - else - phrase = $"reference to {methodRef.DeclaringType.FullName}.{methodRef.Name} (no such method)"; - - this.MarkFlag(InstructionHandleResult.NotCompatible, phrase); - return false; - } - } - - return false; - } - - - /********* - ** Private methods - *********/ - /// Whether references to the given type should be validated. - /// The type reference. - private bool ShouldValidate([NotNullWhen(true)] TypeReference? type) - { - return type != null && this.ValidateReferencesToAssemblies.Contains(type.Scope.Name); - } - - /// Get whether a method reference is a special case that's not currently supported (e.g. array methods). - /// The method reference. - private bool IsUnsupported(MethodReference method) - { - return - method.DeclaringType.Name.Contains("["); // array methods - } - - /// Get whether a method reference is a property getter or setter. - /// The method reference. - private bool IsProperty(MethodReference method) - { - return method.Name.StartsWith("get_") || method.Name.StartsWith("set_"); - } - } -} diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index 284c8a444..cea30f7a7 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -240,8 +240,7 @@ public IEnumerable GetHandlers(bool paranoidMode, bool rewr ** detect mod issues ****/ // broken code - yield return new ReferenceToMissingMemberFinder(this.ValidateReferencesToAssemblies); - yield return new ReferenceToMemberWithUnexpectedTypeFinder(this.ValidateReferencesToAssemblies); + yield return new ReferenceToInvalidMemberFinder(this.ValidateReferencesToAssemblies); // code which may impact game stability yield return new FieldFinder(typeof(SaveGame).FullName!, new[] { nameof(SaveGame.serializer), nameof(SaveGame.farmerSerializer), nameof(SaveGame.locationSerializer) }, InstructionHandleResult.DetectedSaveSerializer);