diff options
Diffstat (limited to 'src/SMAPI/Framework')
6 files changed, 222 insertions, 25 deletions
diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index b5533335..5b5a621b 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -285,31 +285,44 @@ namespace StardewModdingAPI.Framework.ModLoading // find (and optionally rewrite) incompatible instructions bool anyRewritten = false; IInstructionHandler[] handlers = new InstructionMetadata().GetHandlers(this.ParanoidMode).ToArray(); - foreach (MethodDefinition method in this.GetMethods(module)) + foreach (TypeDefinition type in module.GetTypes()) { - // check method definition + // check type definition foreach (IInstructionHandler handler in handlers) { - InstructionHandleResult result = handler.Handle(module, method, this.AssemblyMap, platformChanged); + InstructionHandleResult result = handler.Handle(module, type, this.AssemblyMap, platformChanged); this.ProcessInstructionHandleResult(mod, handler, result, loggedMessages, logPrefix, filename); if (result == InstructionHandleResult.Rewritten) anyRewritten = true; } - // check CIL instructions - ILProcessor cil = method.Body.GetILProcessor(); - var instructions = cil.Body.Instructions; - // ReSharper disable once ForCanBeConvertedToForeach -- deliberate access by index so each handler sees replacements from previous handlers - for (int offset = 0; offset < instructions.Count; offset++) + // check methods + foreach (MethodDefinition method in type.Methods.Where(p => p.HasBody)) { + // check method definition foreach (IInstructionHandler handler in handlers) { - Instruction instruction = instructions[offset]; - InstructionHandleResult result = handler.Handle(module, cil, instruction, this.AssemblyMap, platformChanged); + InstructionHandleResult result = handler.Handle(module, method, this.AssemblyMap, platformChanged); this.ProcessInstructionHandleResult(mod, handler, result, loggedMessages, logPrefix, filename); if (result == InstructionHandleResult.Rewritten) anyRewritten = true; } + + // check CIL instructions + ILProcessor cil = method.Body.GetILProcessor(); + var instructions = cil.Body.Instructions; + // ReSharper disable once ForCanBeConvertedToForeach -- deliberate access by index so each handler sees replacements from previous handlers + for (int offset = 0; offset < instructions.Count; offset++) + { + foreach (IInstructionHandler handler in handlers) + { + Instruction instruction = instructions[offset]; + InstructionHandleResult result = handler.Handle(module, cil, instruction, this.AssemblyMap, platformChanged); + this.ProcessInstructionHandleResult(mod, handler, result, loggedMessages, logPrefix, filename); + if (result == InstructionHandleResult.Rewritten) + anyRewritten = true; + } + } } } @@ -395,18 +408,5 @@ namespace StardewModdingAPI.Framework.ModLoading AssemblyNameReference assemblyRef = this.AssemblyMap.TargetReferences[assembly]; type.Scope = assemblyRef; } - - /// <summary>Get all methods in a module.</summary> - /// <param name="module">The module to search.</param> - private IEnumerable<MethodDefinition> GetMethods(ModuleDefinition module) - { - return ( - from type in module.GetTypes() - where type.HasMethods - from method in type.Methods - where method.HasBody - select method - ); - } } } diff --git a/src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs b/src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs index 10780d07..353de464 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs @@ -18,6 +18,16 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework *********/ /// <summary>Perform the predefined logic for a method if applicable.</summary> /// <param name="module">The assembly module containing the instruction.</param> + /// <param name="type">The type definition to handle.</param> + /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> + /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> + public virtual InstructionHandleResult Handle(ModuleDefinition module, TypeDefinition type, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + return InstructionHandleResult.None; + } + + /// <summary>Perform the predefined logic for a method if applicable.</summary> + /// <param name="module">The assembly module containing the instruction.</param> /// <param name="method">The method definition to handle.</param> /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> diff --git a/src/SMAPI/Framework/ModLoading/Framework/BaseTypeFinder.cs b/src/SMAPI/Framework/ModLoading/Framework/BaseTypeFinder.cs index cfd87922..48165c4c 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/BaseTypeFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/BaseTypeFinder.cs @@ -50,9 +50,38 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework /// <param name="method">The method definition.</param> public bool IsMatch(MethodDefinition method) { + // return type if (this.IsMatch(method.ReturnType)) return true; + // parameters + foreach (ParameterDefinition parameter in method.Parameters) + { + if (this.IsMatch(parameter.ParameterType)) + return true; + } + + // generic parameters + foreach (GenericParameter parameter in method.GenericParameters) + { + if (this.IsMatch(parameter)) + return true; + } + + // custom attributes + foreach (CustomAttribute attribute in method.CustomAttributes) + { + if (this.IsMatch(attribute.AttributeType)) + return true; + + foreach (var arg in attribute.ConstructorArguments) + { + if (this.IsMatch(arg.Type)) + return true; + } + } + + // local variables foreach (VariableDefinition variable in method.Body.Variables) { if (this.IsMatch(variable.VariableType)) diff --git a/src/SMAPI/Framework/ModLoading/Framework/BaseTypeReferenceRewriter.cs b/src/SMAPI/Framework/ModLoading/Framework/BaseTypeReferenceRewriter.cs index 5f38a30b..445ac2cb 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/BaseTypeReferenceRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/BaseTypeReferenceRewriter.cs @@ -1,6 +1,8 @@ using System; +using System.Linq; using Mono.Cecil; using Mono.Cecil.Cil; +using Mono.Collections.Generic; namespace StardewModdingAPI.Framework.ModLoading.Framework { @@ -19,6 +21,20 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework *********/ /// <summary>Perform the predefined logic for a method if applicable.</summary> /// <param name="module">The assembly module containing the instruction.</param> + /// <param name="type">The type definition to handle.</param> + /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> + /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> + public override InstructionHandleResult Handle(ModuleDefinition module, TypeDefinition type, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + bool rewritten = this.RewriteCustomAttributesIfNeeded(module, type.CustomAttributes); + + return rewritten + ? InstructionHandleResult.Rewritten + : InstructionHandleResult.None; + } + + /// <summary>Perform the predefined logic for a method if applicable.</summary> + /// <param name="module">The assembly module containing the instruction.</param> /// <param name="method">The method definition to handle.</param> /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> @@ -28,9 +44,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework // return type if (this.Finder.IsMatch(method.ReturnType)) - { rewritten |= this.RewriteIfNeeded(module, method.ReturnType, newType => method.ReturnType = newType); - } // parameters foreach (ParameterDefinition parameter in method.Parameters) @@ -47,6 +61,9 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework rewritten |= this.RewriteIfNeeded(module, parameter, newType => method.GenericParameters[i] = new GenericParameter(parameter.Name, newType)); } + // custom attributes + rewritten |= this.RewriteCustomAttributesIfNeeded(module, method.CustomAttributes); + // local variables foreach (VariableDefinition variable in method.Body.Variables) { @@ -116,5 +133,72 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework /// <param name="type">The type to replace if it matches.</param> /// <param name="set">Assign the new type reference.</param> protected abstract bool RewriteIfNeeded(ModuleDefinition module, TypeReference type, Action<TypeReference> set); + + /// <summary>Rewrite custom attributes if needed.</summary> + /// <param name="module">The assembly module containing the attributes.</param> + /// <param name="attributes">The custom attributes to handle.</param> + private bool RewriteCustomAttributesIfNeeded(ModuleDefinition module, Collection<CustomAttribute> attributes) + { + bool rewritten = false; + + for (int attrIndex = 0; attrIndex < attributes.Count; attrIndex++) + { + CustomAttribute attribute = attributes[attrIndex]; + bool curChanged = false; + + // attribute type + TypeReference newAttrType = null; + if (this.Finder.IsMatch(attribute.AttributeType)) + { + rewritten |= this.RewriteIfNeeded(module, attribute.AttributeType, newType => + { + newAttrType = newType; + curChanged = true; + }); + } + + // constructor arguments + TypeReference[] argTypes = new TypeReference[attribute.ConstructorArguments.Count]; + for (int i = 0; i < argTypes.Length; i++) + { + var arg = attribute.ConstructorArguments[i]; + + argTypes[i] = arg.Type; + rewritten |= this.RewriteIfNeeded(module, arg.Type, newType => + { + argTypes[i] = newType; + curChanged = true; + }); + } + + // swap attribute + if (curChanged) + { + // get constructor + MethodDefinition constructor = (newAttrType ?? attribute.AttributeType) + .Resolve() + .Methods + .Where(method => method.IsConstructor) + .FirstOrDefault(ctor => RewriteHelper.HasMatchingSignature(ctor, attribute.Constructor)); + if (constructor == null) + throw new InvalidOperationException($"Can't rewrite attribute type '{attribute.AttributeType.FullName}' to '{newAttrType?.FullName}', no equivalent constructor found."); + + // create new attribute + var newAttr = new CustomAttribute(module.ImportReference(constructor)); + for (int i = 0; i < argTypes.Length; i++) + newAttr.ConstructorArguments.Add(new CustomAttributeArgument(argTypes[i], attribute.ConstructorArguments[i].Value)); + foreach (var prop in attribute.Properties) + newAttr.Properties.Add(new CustomAttributeNamedArgument(prop.Name, prop.Argument)); + foreach (var field in attribute.Fields) + newAttr.Fields.Add(new CustomAttributeNamedArgument(field.Name, field.Argument)); + + // swap attribute + attributes[attrIndex] = newAttr; + rewritten = true; + } + } + + return rewritten; + } } } diff --git a/src/SMAPI/Framework/ModLoading/IInstructionHandler.cs b/src/SMAPI/Framework/ModLoading/IInstructionHandler.cs index 65b45b08..f9d320a6 100644 --- a/src/SMAPI/Framework/ModLoading/IInstructionHandler.cs +++ b/src/SMAPI/Framework/ModLoading/IInstructionHandler.cs @@ -18,6 +18,13 @@ namespace StardewModdingAPI.Framework.ModLoading *********/ /// <summary>Perform the predefined logic for a method if applicable.</summary> /// <param name="module">The assembly module containing the instruction.</param> + /// <param name="type">The type definition to handle.</param> + /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> + /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> + InstructionHandleResult Handle(ModuleDefinition module, TypeDefinition type, PlatformAssemblyMap assemblyMap, bool platformChanged); + + /// <summary>Perform the predefined logic for a method if applicable.</summary> + /// <param name="module">The assembly module containing the instruction.</param> /// <param name="method">The method definition to handle.</param> /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> diff --git a/src/SMAPI/Framework/ModLoading/RewriteHelper.cs b/src/SMAPI/Framework/ModLoading/RewriteHelper.cs index d9a49cfa..553679f9 100644 --- a/src/SMAPI/Framework/ModLoading/RewriteHelper.cs +++ b/src/SMAPI/Framework/ModLoading/RewriteHelper.cs @@ -42,6 +42,10 @@ namespace StardewModdingAPI.Framework.ModLoading /// <param name="reference">The type reference.</param> public static bool IsSameType(Type type, TypeReference reference) { + // + // duplicated by IsSameType(TypeReference, TypeReference) below + // + // same namespace & name if (type.Namespace != reference.Namespace || type.Name != reference.Name) return false; @@ -66,6 +70,39 @@ namespace StardewModdingAPI.Framework.ModLoading return true; } + /// <summary>Get whether a type matches a type reference.</summary> + /// <param name="type">The defined type.</param> + /// <param name="reference">The type reference.</param> + public static bool IsSameType(TypeReference type, TypeReference reference) + { + // + // duplicated by IsSameType(Type, TypeReference) above + // + + // same namespace & name + if (type.Namespace != reference.Namespace || type.Name != reference.Name) + return false; + + // same generic parameters + if (type.IsGenericInstance) + { + if (!reference.IsGenericInstance) + return false; + + TypeReference[] defGenerics = ((GenericInstanceType)type).GenericArguments.ToArray(); + TypeReference[] refGenerics = ((GenericInstanceType)reference).GenericArguments.ToArray(); + if (defGenerics.Length != refGenerics.Length) + return false; + for (int i = 0; i < defGenerics.Length; i++) + { + if (!RewriteHelper.IsSameType(defGenerics[i], refGenerics[i])) + return false; + } + } + + return true; + } + /// <summary>Determine whether two type IDs look like the same type, accounting for placeholder values such as !0.</summary> /// <param name="typeA">The type ID to compare.</param> /// <param name="typeB">The other type ID to compare.</param> @@ -80,6 +117,10 @@ namespace StardewModdingAPI.Framework.ModLoading /// <param name="reference">The method reference.</param> public static bool HasMatchingSignature(MethodInfo definition, MethodReference reference) { + // + // duplicated by HasMatchingSignature(MethodDefinition, MethodReference) below + // + // same name if (definition.Name != reference.Name) return false; @@ -97,6 +138,32 @@ namespace StardewModdingAPI.Framework.ModLoading return true; } + /// <summary>Get whether a method definition matches the signature expected by a method reference.</summary> + /// <param name="definition">The method definition.</param> + /// <param name="reference">The method reference.</param> + public static bool HasMatchingSignature(MethodDefinition definition, MethodReference reference) + { + // + // duplicated by HasMatchingSignature(MethodInfo, MethodReference) above + // + + // same name + if (definition.Name != reference.Name) + return false; + + // same arguments + ParameterDefinition[] definitionParameters = definition.Parameters.ToArray(); + ParameterDefinition[] referenceParameters = reference.Parameters.ToArray(); + if (referenceParameters.Length != definitionParameters.Length) + return false; + for (int i = 0; i < referenceParameters.Length; i++) + { + if (!RewriteHelper.IsSameType(definitionParameters[i].ParameterType, referenceParameters[i].ParameterType)) + return false; + } + return true; + } + /// <summary>Get whether a type has a method whose signature matches the one expected by a method reference.</summary> /// <param name="type">The type to check.</param> /// <param name="reference">The method reference.</param> |