diff options
author | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2020-09-07 13:06:27 -0400 |
---|---|---|
committer | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2020-09-07 13:06:27 -0400 |
commit | 5e43bdbf5cd6dbab36c25287c85d42ccfeea2c83 (patch) | |
tree | 0a42305174eb84561a584549cd685c5e95670f36 /src/SMAPI/Framework/ModLoading/Rewriters | |
parent | 8da88b8fe5b41739c5cd0df3280b9770fc7f10a4 (diff) | |
parent | f9fac11028354f15d786d5b854608edb10716f79 (diff) | |
download | SMAPI-5e43bdbf5cd6dbab36c25287c85d42ccfeea2c83.tar.gz SMAPI-5e43bdbf5cd6dbab36c25287c85d42ccfeea2c83.tar.bz2 SMAPI-5e43bdbf5cd6dbab36c25287c85d42ccfeea2c83.zip |
Merge branch 'develop' into stable
Diffstat (limited to 'src/SMAPI/Framework/ModLoading/Rewriters')
8 files changed, 240 insertions, 177 deletions
diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs index 8043b13a..0b679e9d 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs @@ -26,26 +26,31 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters ** Public methods *********/ /// <summary>Construct an instance.</summary> - /// <param name="type">The type whose field to rewrite.</param> + /// <param name="fromType">The type whose field to rewrite.</param> /// <param name="fromFieldName">The field name to rewrite.</param> + /// <param name="toType">The new type which will have the field.</param> /// <param name="toFieldName">The new field name to reference.</param> - public FieldReplaceRewriter(Type type, string fromFieldName, string toFieldName) - : base(defaultPhrase: $"{type.FullName}.{fromFieldName} field") + public FieldReplaceRewriter(Type fromType, string fromFieldName, Type toType, string toFieldName) + : base(defaultPhrase: $"{fromType.FullName}.{fromFieldName} field") { - this.Type = type; + this.Type = fromType; this.FromFieldName = fromFieldName; - this.ToField = type.GetField(toFieldName); + this.ToField = toType.GetField(toFieldName); if (this.ToField == null) - throw new InvalidOperationException($"The {type.FullName} class doesn't have a {toFieldName} field."); + throw new InvalidOperationException($"The {toType.FullName} class doesn't have a {toFieldName} field."); } - /// <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) + /// <summary>Construct an instance.</summary> + /// <param name="type">The type whose field to rewrite.</param> + /// <param name="fromFieldName">The field name to rewrite.</param> + /// <param name="toFieldName">The new field name to reference.</param> + public FieldReplaceRewriter(Type type, string fromFieldName, string toFieldName) + : this(type, fromFieldName, type, toFieldName) + { + } + + /// <inheritdoc /> + public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction) { // get field reference FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); @@ -53,8 +58,8 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters return false; // replace with new field - FieldReference newRef = module.ImportReference(this.ToField); - replaceWith(cil.Create(instruction.OpCode, newRef)); + instruction.Operand = module.ImportReference(this.ToField); + return this.MarkRewritten(); } } diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs deleted file mode 100644 index c3b5854e..00000000 --- a/src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using Mono.Cecil; -using Mono.Cecil.Cil; -using StardewModdingAPI.Framework.ModLoading.Framework; - -namespace StardewModdingAPI.Framework.ModLoading.Rewriters -{ - /// <summary>Rewrites field references into property references.</summary> - internal class FieldToPropertyRewriter : BaseInstructionHandler - { - /********* - ** Fields - *********/ - /// <summary>The type containing the field to which references should be rewritten.</summary> - private readonly Type Type; - - /// <summary>The field name to which references should be rewritten.</summary> - private readonly string FromFieldName; - - /// <summary>The new property name.</summary> - private readonly string ToPropertyName; - - - /********* - ** Public methods - *********/ - /// <summary>Construct an instance.</summary> - /// <param name="type">The type whose field to which references should be rewritten.</param> - /// <param name="fieldName">The field name to rewrite.</param> - /// <param name="propertyName">The property name (if different).</param> - public FieldToPropertyRewriter(Type type, string fieldName, string propertyName) - : base(defaultPhrase: $"{type.FullName}.{fieldName} field") - { - this.Type = type; - this.FromFieldName = fieldName; - this.ToPropertyName = propertyName; - } - - /// <summary>Construct an instance.</summary> - /// <param name="type">The type whose field to which references should be rewritten.</param> - /// <param name="fieldName">The field name to rewrite.</param> - public FieldToPropertyRewriter(Type type, string fieldName) - : this(type, fieldName, fieldName) { } - - /// <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 field ref - FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); - if (!RewriteHelper.IsFieldReferenceTo(fieldRef, this.Type.FullName, this.FromFieldName)) - return false; - - // replace with property - string methodPrefix = instruction.OpCode == OpCodes.Ldsfld || instruction.OpCode == OpCodes.Ldfld ? "get" : "set"; - MethodReference propertyRef = module.ImportReference(this.Type.GetMethod($"{methodPrefix}_{this.ToPropertyName}")); - replaceWith(cil.Create(OpCodes.Call, propertyRef)); - return this.MarkRewritten(); - } - } -} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs index b30d686e..4b3675bc 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs @@ -25,11 +25,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters public Harmony1AssemblyRewriter() : base(defaultPhrase: "Harmony 1.x") { } - /// <summary>Rewrite a type reference if needed.</summary> - /// <param name="module">The assembly module containing the instruction.</param> - /// <param name="type">The type definition to handle.</param> - /// <param name="replaceWith">Replaces the type reference with a new one.</param> - /// <returns>Returns whether the type was changed.</returns> + /// <inheritdoc /> public override bool Handle(ModuleDefinition module, TypeReference type, Action<TypeReference> replaceWith) { // rewrite Harmony 1.x type to Harmony 2.0 type @@ -45,12 +41,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters return false; } - /// <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> + /// <inheritdoc /> public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action<Instruction> replaceWith) { // rewrite Harmony 1.x methods to Harmony 2.0 diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs new file mode 100644 index 00000000..ca04205c --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs @@ -0,0 +1,106 @@ +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>Automatically fix references to fields that have been replaced by a property or const field.</summary> + internal class HeuristicFieldRewriter : 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 HeuristicFieldRewriter(string[] rewriteReferencesToAssemblies) + : base(defaultPhrase: "field changed to property") // ignored since we specify phrases + { + this.RewriteReferencesToAssemblies = new HashSet<string>(rewriteReferencesToAssemblies); + } + + /// <inheritdoc /> + public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction) + { + // get field ref + FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); + if (fieldRef == null || !this.ShouldValidate(fieldRef.DeclaringType)) + return false; + + // skip if not broken + FieldDefinition fieldDefinition = fieldRef.Resolve(); + if (fieldDefinition != null && !fieldDefinition.HasConstant) + return false; + + // rewrite if possible + TypeDefinition declaringType = fieldRef.DeclaringType.Resolve(); + bool isRead = instruction.OpCode == OpCodes.Ldsfld || instruction.OpCode == OpCodes.Ldfld; + return + this.TryRewriteToProperty(module, instruction, fieldRef, declaringType, isRead) + || this.TryRewriteToConstField(instruction, fieldDefinition); + } + + + /********* + ** 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>Try rewriting the field into a matching property.</summary> + /// <param name="module">The assembly module containing the instruction.</param> + /// <param name="instruction">The CIL instruction to rewrite.</param> + /// <param name="fieldRef">The field reference.</param> + /// <param name="declaringType">The type on which the field was defined.</param> + /// <param name="isRead">Whether the field is being read; else it's being written to.</param> + private bool TryRewriteToProperty(ModuleDefinition module, Instruction instruction, FieldReference fieldRef, TypeDefinition declaringType, bool isRead) + { + // get equivalent property + PropertyDefinition property = declaringType.Properties.FirstOrDefault(p => p.Name == fieldRef.Name); + MethodDefinition method = isRead ? property?.GetMethod : property?.SetMethod; + if (method == null) + return false; + + // rewrite field to property + instruction.OpCode = OpCodes.Call; + instruction.Operand = module.ImportReference(method); + + this.Phrases.Add($"{fieldRef.DeclaringType.Name}.{fieldRef.Name} (field => property)"); + return this.MarkRewritten(); + } + + /// <summary>Try rewriting the field into a matching const field.</summary> + /// <param name="instruction">The CIL instruction to rewrite.</param> + /// <param name="field">The field definition.</param> + private bool TryRewriteToConstField(Instruction instruction, FieldDefinition field) + { + // must have been a static field read, and the new field must be const + if (instruction.OpCode != OpCodes.Ldsfld || field?.HasConstant != true) + return false; + + // get opcode for value type + Instruction loadInstruction = RewriteHelper.GetLoadValueInstruction(field.Constant); + if (loadInstruction == null) + return false; + + // rewrite to constant + instruction.OpCode = loadInstruction.OpCode; + instruction.Operand = loadInstruction.Operand; + + this.Phrases.Add($"{field.DeclaringType.Name}.{field.Name} (field => const)"); + return this.MarkRewritten(); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicMethodRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicMethodRewriter.cs new file mode 100644 index 00000000..e133b6fa --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicMethodRewriter.cs @@ -0,0 +1,109 @@ +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>Automatically fix references to methods that had extra optional parameters added.</summary> + internal class HeuristicMethodRewriter : 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 HeuristicMethodRewriter(string[] rewriteReferencesToAssemblies) + : base(defaultPhrase: "methods with missing parameters") // ignored since we specify phrases + { + this.RewriteReferencesToAssemblies = new HashSet<string>(rewriteReferencesToAssemblies); + } + + /// <inheritdoc /> + public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction) + { + // 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; + + // get instructions to inject parameter values + var loadInstructions = method.Parameters.Skip(methodRef.Parameters.Count) + .Select(p => RewriteHelper.GetLoadValueInstruction(p.Constant)) + .ToArray(); + if (loadInstructions.Any(p => p == null)) + return false; // SMAPI needs to load the value onto the stack before the method call, but the optional parameter type wasn't recognized + + // rewrite method reference + foreach (Instruction loadInstruction in loadInstructions) + cil.InsertBefore(instruction, loadInstruction); + instruction.Operand = module.ImportReference(method); + + 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/Framework/ModLoading/Rewriters/MethodParentRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs index b8e53f40..9933e2ca 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs @@ -40,13 +40,8 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters public MethodParentRewriter(Type fromType, Type toType, string nounPhrase = null) : this(fromType.FullName, toType, nounPhrase) { } - /// <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) + /// <inheritdoc /> + public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction) { // get method ref MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StaticFieldToConstantRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StaticFieldToConstantRewriter.cs deleted file mode 100644 index 6ef18b26..00000000 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StaticFieldToConstantRewriter.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using Mono.Cecil; -using Mono.Cecil.Cil; -using StardewModdingAPI.Framework.ModLoading.Framework; - -namespace StardewModdingAPI.Framework.ModLoading.Rewriters -{ - /// <summary>Rewrites static field references into constant values.</summary> - /// <typeparam name="TValue">The constant value type.</typeparam> - internal class StaticFieldToConstantRewriter<TValue> : BaseInstructionHandler - { - /********* - ** Fields - *********/ - /// <summary>The type containing the field to which references should be rewritten.</summary> - private readonly Type Type; - - /// <summary>The field name to which references should be rewritten.</summary> - private readonly string FromFieldName; - - /// <summary>The constant value to replace with.</summary> - private readonly TValue Value; - - - /********* - ** Public methods - *********/ - /// <summary>Construct an instance.</summary> - /// <param name="type">The type whose field to which references should be rewritten.</param> - /// <param name="fieldName">The field name to rewrite.</param> - /// <param name="value">The constant value to replace with.</param> - public StaticFieldToConstantRewriter(Type type, string fieldName, TValue value) - : base(defaultPhrase: $"{type.FullName}.{fieldName} field") - { - this.Type = type; - this.FromFieldName = fieldName; - this.Value = value; - } - - /// <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 field reference - FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); - if (!RewriteHelper.IsFieldReferenceTo(fieldRef, this.Type.FullName, this.FromFieldName)) - return false; - - // rewrite to constant - replaceWith(this.CreateConstantInstruction(cil, this.Value)); - return this.MarkRewritten(); - } - - - /********* - ** Private methods - *********/ - /// <summary>Create a CIL constant value instruction.</summary> - /// <param name="cil">The CIL processor.</param> - /// <param name="value">The constant value to set.</param> - private Instruction CreateConstantInstruction(ILProcessor cil, object value) - { - if (typeof(TValue) == typeof(int)) - return cil.Create(OpCodes.Ldc_I4, (int)value); - if (typeof(TValue) == typeof(string)) - return cil.Create(OpCodes.Ldstr, (string)value); - throw new NotSupportedException($"Rewriting to constant values of type {typeof(TValue)} isn't currently supported."); - } - } -} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs index c2120444..ad5cb96f 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs @@ -35,11 +35,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters this.ShouldIgnore = shouldIgnore; } - /// <summary>Rewrite a type reference if needed.</summary> - /// <param name="module">The assembly module containing the instruction.</param> - /// <param name="type">The type definition to handle.</param> - /// <param name="replaceWith">Replaces the type reference with a new one.</param> - /// <returns>Returns whether the type was changed.</returns> + /// <inheritdoc /> public override bool Handle(ModuleDefinition module, TypeReference type, Action<TypeReference> replaceWith) { // check type reference |