diff options
-rw-r--r-- | docs/release-notes.md | 5 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModLoading/Rewriters/MethodWithMissingOptionalParameterRewriter.cs | 113 | ||||
-rw-r--r-- | src/SMAPI/Metadata/InstructionMetadata.cs | 7 |
3 files changed, 120 insertions, 5 deletions
diff --git a/docs/release-notes.md b/docs/release-notes.md index efab21d5..7e928aed 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,7 +9,7 @@ ## Upcoming release * For players: - * Added heuristic compatibility rewrites for some common cases. That fixes some mods which previously broke on Android or in newer game versions. + * Added heuristic compatibility rewrites, which fix some mods previously incompatible with Android or newer game versions. * Tweaked the rules for showing update alerts (see _for SMAPI developers_ below for details). * Fixed crossplatform compatibility for mods which use the `[HarmonyPatch(type)]` attribute (thanks to spacechase0!). * Fixed broken URL in update alerts for unofficial versions. @@ -27,7 +27,8 @@ * The web API now returns an update alert in two new cases: any newer unofficial update (previously only shown if the mod was incompatible), and a newer prerelease version if the installed non-prerelease version is broken (previously only shown if the installed version was prerelease). * Internal refactoring to simplify game updates: * Reorganised SMAPI core to reduce coupling to `Game1` and make it easier to navigate. - * `FieldToPropertyRewriter` now auto-rewrites broken field references into properties if possible, so we no longer need to map fields manually. + * Added rewriter for any method broken due to new optional parameters. + * Added rewriter for any field which was replaced by a property. * `FieldReplaceRewriter` now supports mapping to a different target type. * Internal refactoring to simplify future game updates. diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/MethodWithMissingOptionalParameterRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/MethodWithMissingOptionalParameterRewriter.cs new file mode 100644 index 00000000..9db3c3fd --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/MethodWithMissingOptionalParameterRewriter.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Mono.Cecil; +using Mono.Cecil.Cil; +using StardewModdingAPI.Framework.ModLoading.Framework; + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters +{ + /// <summary>Rewrites references to methods which only broke because the definition has new optional parameters.</summary> + internal class MethodWithMissingOptionalParameterRewriter : BaseInstructionHandler + { + /********* + ** Fields + *********/ + /// <summary>The assembly names to which to rewrite broken references.</summary> + private readonly HashSet<string> RewriteReferencesToAssemblies; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="rewriteReferencesToAssemblies">The assembly names to which to rewrite broken references.</param> + public MethodWithMissingOptionalParameterRewriter(string[] rewriteReferencesToAssemblies) + : base(defaultPhrase: "methods with missing parameters") // ignored since we specify phrases + { + this.RewriteReferencesToAssemblies = new HashSet<string>(rewriteReferencesToAssemblies); + } + + /// <summary>Rewrite a CIL instruction reference if needed.</summary> + /// <param name="module">The assembly module containing the instruction.</param> + /// <param name="cil">The CIL processor.</param> + /// <param name="instruction">The CIL instruction to handle.</param> + /// <param name="replaceWith">Replaces the CIL instruction with a new one.</param> + /// <returns>Returns whether the instruction was changed.</returns> + public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action<Instruction> replaceWith) + { + // get method ref + MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); + if (methodRef == null || !this.ShouldValidate(methodRef.DeclaringType)) + return false; + + // skip if not broken + if (methodRef.Resolve() != null) + return false; + + // get type + var type = methodRef.DeclaringType.Resolve(); + if (type == null) + return false; + + // get method definition + MethodDefinition method = null; + foreach (var match in type.Methods.Where(p => p.Name == methodRef.Name)) + { + // reference matches initial parameters of definition + if (methodRef.Parameters.Count >= match.Parameters.Count || !this.InitialParametersMatch(methodRef, match)) + continue; + + // all remaining parameters in definition are optional + if (!match.Parameters.Skip(methodRef.Parameters.Count).All(p => p.IsOptional)) + continue; + + method = match; + break; + } + if (method == null) + return false; + + // add extra parameters + foreach (ParameterDefinition parameter in method.Parameters.Skip(methodRef.Parameters.Count)) + { + methodRef.Parameters.Add(new ParameterDefinition( + name: parameter.Name, + attributes: parameter.Attributes, + parameterType: module.ImportReference(parameter.ParameterType) + )); + } + + this.Phrases.Add($"{methodRef.DeclaringType.Name}.{methodRef.Name} (added missing optional parameters)"); + return this.MarkRewritten(); + } + + + /********* + ** Private methods + *********/ + /// <summary>Whether references to the given type should be validated.</summary> + /// <param name="type">The type reference.</param> + private bool ShouldValidate(TypeReference type) + { + return type != null && this.RewriteReferencesToAssemblies.Contains(type.Scope.Name); + } + + /// <summary>Get whether every parameter in the method reference matches the exact order and type of the parameters in the method definition. This ignores extra parameters in the definition.</summary> + /// <param name="methodRef">The method reference whose parameters to check.</param> + /// <param name="method">The method definition whose parameters to check against.</param> + private bool InitialParametersMatch(MethodReference methodRef, MethodDefinition method) + { + if (methodRef.Parameters.Count > method.Parameters.Count) + return false; + + for (int i = 0; i < methodRef.Parameters.Count; i++) + { + if (!RewriteHelper.IsSameType(methodRef.Parameters[i].ParameterType, method.Parameters[i].ParameterType)) + return false; + } + + return true; + } + } +} diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index fca809f8..972ed91d 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -31,9 +31,6 @@ namespace StardewModdingAPI.Metadata /**** ** rewrite CIL to fix incompatible code ****/ - // generic rewrites - yield return new FieldToPropertyRewriter(this.ValidateReferencesToAssemblies); - // rewrite for crossplatform compatibility if (platformChanged) yield return new MethodParentRewriter(typeof(SpriteBatch), typeof(SpriteBatchFacade)); @@ -41,6 +38,10 @@ namespace StardewModdingAPI.Metadata // rewrite for Stardew Valley 1.3 yield return new StaticFieldToConstantRewriter<int>(typeof(Game1), "tileSize", Game1.tileSize); + // generic rewrites + yield return new FieldToPropertyRewriter(this.ValidateReferencesToAssemblies); + yield return new MethodWithMissingOptionalParameterRewriter(this.ValidateReferencesToAssemblies); + #if HARMONY_2 // rewrite for SMAPI 3.6 (Harmony 1.x => 2.0 update) yield return new Harmony1AssemblyRewriter(); |