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);