diff options
Diffstat (limited to 'src/SMAPI/Framework/ModLoading')
24 files changed, 418 insertions, 396 deletions
diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index f8c901e0..9fb5384e 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -76,10 +76,9 @@ namespace StardewModdingAPI.Framework.ModLoading /// <param name="mod">The mod for which the assembly is being loaded.</param> /// <param name="assemblyPath">The assembly file path.</param> /// <param name="assumeCompatible">Assume the mod is compatible, even if incompatible code is detected.</param> - /// <param name="rewriteInParallel">Whether to enable experimental parallel rewriting.</param> /// <returns>Returns the rewrite metadata for the preprocessed assembly.</returns> /// <exception cref="IncompatibleInstructionException">An incompatible CIL instruction was found while rewriting the assembly.</exception> - public Assembly Load(IModMetadata mod, string assemblyPath, bool assumeCompatible, bool rewriteInParallel) + public Assembly Load(IModMetadata mod, string assemblyPath, bool assumeCompatible) { // get referenced local assemblies AssemblyParseResult[] assemblies; @@ -109,7 +108,7 @@ namespace StardewModdingAPI.Framework.ModLoading continue; // rewrite assembly - bool changed = this.RewriteAssembly(mod, assembly.Definition, loggedMessages, logPrefix: " ", rewriteInParallel); + bool changed = this.RewriteAssembly(mod, assembly.Definition, loggedMessages, logPrefix: " "); // detect broken assembly reference foreach (AssemblyNameReference reference in assembly.Definition.MainModule.AssemblyReferences) @@ -263,10 +262,9 @@ namespace StardewModdingAPI.Framework.ModLoading /// <param name="assembly">The assembly to rewrite.</param> /// <param name="loggedMessages">The messages that have already been logged for this mod.</param> /// <param name="logPrefix">A string to prefix to log messages.</param> - /// <param name="rewriteInParallel">Whether to enable experimental parallel rewriting.</param> /// <returns>Returns whether the assembly was modified.</returns> /// <exception cref="IncompatibleInstructionException">An incompatible CIL instruction was found while rewriting the assembly.</exception> - private bool RewriteAssembly(IModMetadata mod, AssemblyDefinition assembly, HashSet<string> loggedMessages, string logPrefix, bool rewriteInParallel) + private bool RewriteAssembly(IModMetadata mod, AssemblyDefinition assembly, HashSet<string> loggedMessages, string logPrefix) { ModuleDefinition module = assembly.MainModule; string filename = $"{assembly.Name.Name}.dll"; @@ -294,6 +292,19 @@ namespace StardewModdingAPI.Framework.ModLoading IEnumerable<TypeReference> typeReferences = module.GetTypeReferences().OrderBy(p => p.FullName); foreach (TypeReference type in typeReferences) this.ChangeTypeScope(type); + + // rewrite types using custom attributes + foreach (TypeDefinition type in module.GetTypes()) + { + foreach (var attr in type.CustomAttributes) + { + foreach (var conField in attr.ConstructorArguments) + { + if (conField.Value is TypeReference typeRef) + this.ChangeTypeScope(typeRef); + } + } + } } // find or rewrite code @@ -307,15 +318,15 @@ namespace StardewModdingAPI.Framework.ModLoading rewritten |= handler.Handle(module, type, replaceWith); return rewritten; }, - rewriteInstruction: (ref Instruction instruction, ILProcessor cil, Action<Instruction> replaceWith) => + rewriteInstruction: (ref Instruction instruction, ILProcessor cil) => { bool rewritten = false; foreach (IInstructionHandler handler in handlers) - rewritten |= handler.Handle(module, cil, instruction, replaceWith); + rewritten |= handler.Handle(module, cil, instruction); return rewritten; } ); - bool anyRewritten = rewriter.RewriteModule(rewriteInParallel); + bool anyRewritten = rewriter.RewriteModule(); // handle rewrite flags foreach (IInstructionHandler handler in handlers) @@ -398,10 +409,10 @@ namespace StardewModdingAPI.Framework.ModLoading if (handler.Phrases.Any()) { foreach (string message in handler.Phrases) - this.Monitor.LogOnce(template.Replace("$phrase", message)); + this.Monitor.LogOnce(loggedMessages, template.Replace("$phrase", message)); } else - this.Monitor.LogOnce(template.Replace("$phrase", handler.DefaultPhrase ?? handler.GetType().Name)); + this.Monitor.LogOnce(loggedMessages, template.Replace("$phrase", handler.DefaultPhrase ?? handler.GetType().Name)); } /// <summary>Get the correct reference to use for compatibility with the current platform.</summary> diff --git a/src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs index e1476b73..01ed153b 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs @@ -1,4 +1,3 @@ -using System; using Mono.Cecil; using Mono.Cecil.Cil; using StardewModdingAPI.Framework.ModLoading.Framework; @@ -36,13 +35,8 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders this.Result = result; } - /// <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) { if (!this.Flags.Contains(this.Result) && this.IsMatch(instruction)) this.MarkFlag(this.Result); diff --git a/src/SMAPI/Framework/ModLoading/Finders/FieldFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/FieldFinder.cs index c157ed9b..2c062243 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/FieldFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/FieldFinder.cs @@ -1,4 +1,3 @@ -using System; using Mono.Cecil; using Mono.Cecil.Cil; using StardewModdingAPI.Framework.ModLoading.Framework; @@ -36,13 +35,8 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders this.Result = result; } - /// <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) { if (!this.Flags.Contains(this.Result) && RewriteHelper.IsFieldReferenceTo(instruction, this.FullTypeName, this.FieldName)) this.MarkFlag(this.Result); diff --git a/src/SMAPI/Framework/ModLoading/Finders/MethodFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/MethodFinder.cs index 82c93a7c..d2340f01 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/MethodFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/MethodFinder.cs @@ -1,4 +1,3 @@ -using System; using Mono.Cecil; using Mono.Cecil.Cil; using StardewModdingAPI.Framework.ModLoading.Framework; @@ -36,13 +35,8 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders this.Result = result; } - /// <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) { if (!this.Flags.Contains(this.Result) && this.IsMatch(instruction)) this.MarkFlag(this.Result); diff --git a/src/SMAPI/Framework/ModLoading/Finders/PropertyFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/PropertyFinder.cs index c96d61a2..99344848 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/PropertyFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/PropertyFinder.cs @@ -1,4 +1,3 @@ -using System; using Mono.Cecil; using Mono.Cecil.Cil; using StardewModdingAPI.Framework.ModLoading.Framework; @@ -36,13 +35,8 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders this.Result = result; } - /// <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) { if (!this.Flags.Contains(this.Result) && this.IsMatch(instruction)) this.MarkFlag(this.Result); diff --git a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs index a67cfa4f..b01a3240 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Linq; using Mono.Cecil; @@ -29,13 +28,8 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders this.ValidateReferencesToAssemblies = new HashSet<string>(validateReferencesToAssemblies); } - /// <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) { // field reference FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); diff --git a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs index ebb62948..b64a255e 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs @@ -1,6 +1,4 @@ -using System; using System.Collections.Generic; -using System.Linq; using Mono.Cecil; using Mono.Cecil.Cil; using StardewModdingAPI.Framework.ModLoading.Framework; @@ -29,20 +27,15 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders this.ValidateReferencesToAssemblies = new HashSet<string>(validateReferencesToAssemblies); } - /// <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) { // field reference FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); if (fieldRef != null && this.ShouldValidate(fieldRef.DeclaringType)) { - FieldDefinition target = fieldRef.DeclaringType.Resolve()?.Fields.FirstOrDefault(p => p.Name == fieldRef.Name); - if (target == null) + FieldDefinition target = fieldRef.Resolve(); + if (target == null || target.HasConstant) { this.MarkFlag(InstructionHandleResult.NotCompatible, $"reference to {fieldRef.DeclaringType.FullName}.{fieldRef.Name} (no such field)"); return false; @@ -56,7 +49,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders MethodDefinition target = methodRef.Resolve(); if (target == null) { - string phrase = 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") diff --git a/src/SMAPI/Framework/ModLoading/Finders/TypeAssemblyFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/TypeAssemblyFinder.cs index a1ade536..24ab2eca 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/TypeAssemblyFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/TypeAssemblyFinder.cs @@ -35,11 +35,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders 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) { if (type.Scope.Name == this.AssemblyName && this.ShouldIgnore?.Invoke(type) != true) diff --git a/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs index c285414a..bbd081e8 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs @@ -35,11 +35,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders 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) { if (type.FullName == this.FullTypeName && this.ShouldIgnore?.Invoke(type) != true) diff --git a/src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs b/src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs index fde37d68..624113b3 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs @@ -11,36 +11,27 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework /********* ** Accessors *********/ - /// <summary>A brief noun phrase indicating what the handler matches, used if <see cref="Phrases"/> is empty.</summary> + /// <inheritdoc /> public string DefaultPhrase { get; } - /// <summary>The rewrite flags raised for the current module.</summary> + /// <inheritdoc /> public ISet<InstructionHandleResult> Flags { get; } = new HashSet<InstructionHandleResult>(); - /// <summary>The brief noun phrases indicating what the handler matched for the current module.</summary> + /// <inheritdoc /> public ISet<string> Phrases { get; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase); /********* ** Public methods *********/ - /// <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 virtual bool Handle(ModuleDefinition module, TypeReference type, Action<TypeReference> replaceWith) { 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> - public virtual bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action<Instruction> replaceWith) + /// <inheritdoc /> + public virtual bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction) { return false; } @@ -50,7 +41,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework ** Protected methods *********/ /// <summary>Construct an instance.</summary> - /// <param name="defaultPhrase">A brief noun phrase indicating what the handler matches.</param> + /// <param name="defaultPhrase">A brief noun phrase indicating what the handler matches, used if <see cref="Phrases"/> is empty.</param> protected BaseInstructionHandler(string defaultPhrase) { this.DefaultPhrase = defaultPhrase; diff --git a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs index 34c78c7d..ea29550a 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading; -using System.Threading.Tasks; using Mono.Cecil; using Mono.Cecil.Cil; using Mono.Collections.Generic; @@ -24,9 +22,8 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework /// <summary>Rewrite a CIL instruction in the assembly code.</summary> /// <param name="instruction">The current CIL instruction.</param> /// <param name="cil">The CIL instruction processor.</param> - /// <param name="replaceWith">Replaces the CIL instruction with the given instruction.</param> /// <returns>Returns whether the instruction was changed.</returns> - public delegate bool RewriteInstructionDelegate(ref Instruction instruction, ILProcessor cil, Action<Instruction> replaceWith); + public delegate bool RewriteInstructionDelegate(ref Instruction instruction, ILProcessor cil); /********* @@ -57,59 +54,24 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework } /// <summary>Rewrite the loaded module code.</summary> - /// <param name="rewriteInParallel">Whether to enable experimental parallel rewriting.</param> /// <returns>Returns whether the module was modified.</returns> - public bool RewriteModule(bool rewriteInParallel) + public bool RewriteModule() { IEnumerable<TypeDefinition> types = this.Module.GetTypes().Where(type => type.BaseType != null); // skip special types like <Module> - // experimental parallel rewriting - // This may cause intermittent startup errors and is disabled by default: https://github.com/Pathoschild/SMAPI/issues/721 - if (rewriteInParallel) - { - int typesChanged = 0; - Exception exception = null; - - Parallel.ForEach(types, type => - { - if (exception != null) - return; - - bool changed = false; - try - { - changed = this.RewriteTypeDefinition(type); - } - catch (Exception ex) - { - exception ??= ex; - } - - if (changed) - Interlocked.Increment(ref typesChanged); - }); + bool changed = false; - return exception == null - ? typesChanged > 0 - : throw new Exception($"Rewriting {this.Module.Name} failed.", exception); + try + { + foreach (var type in types) + changed |= this.RewriteTypeDefinition(type); } - - // non-parallel rewriting + catch (Exception ex) { - bool changed = false; - - try - { - foreach (var type in types) - changed |= this.RewriteTypeDefinition(type); - } - catch (Exception ex) - { - throw new Exception($"Rewriting {this.Module.Name} failed.", ex); - } - - return changed; + throw new Exception($"Rewriting {this.Module.Name} failed.", ex); } + + return changed; } @@ -198,12 +160,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework // instruction itself // (should be done after the above type rewrites to ensure valid types) - rewritten |= this.RewriteInstructionImpl(ref instruction, cil, newInstruction => - { - rewritten = true; - cil.Replace(instruction, newInstruction); - instruction = newInstruction; - }); + rewritten |= this.RewriteInstructionImpl(ref instruction, cil); return rewritten; } diff --git a/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs b/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs index 36058b86..207b6445 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs @@ -59,12 +59,30 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework : null; } + /// <summary>Get the CIL instruction to load a value onto the stack.</summary> + /// <param name="rawValue">The constant value to inject.</param> + /// <returns>Returns the instruction, or <c>null</c> if the value type isn't supported.</returns> + public static Instruction GetLoadValueInstruction(object rawValue) + { + return rawValue switch + { + null => Instruction.Create(OpCodes.Ldnull), + bool value => Instruction.Create(value ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0), + int value => Instruction.Create(OpCodes.Ldc_I4, value), // int32 + long value => Instruction.Create(OpCodes.Ldc_I8, value), // int64 + float value => Instruction.Create(OpCodes.Ldc_R4, value), // float32 + double value => Instruction.Create(OpCodes.Ldc_R8, value), // float64 + string value => Instruction.Create(OpCodes.Ldstr, value), + _ => null + }; + } + /// <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(Type type, TypeReference reference) { - // + // // duplicated by IsSameType(TypeReference, TypeReference) below // @@ -139,7 +157,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework /// <param name="reference">The method reference.</param> public static bool HasMatchingSignature(MethodBase definition, MethodReference reference) { - // + // // duplicated by HasMatchingSignature(MethodDefinition, MethodReference) below // @@ -165,7 +183,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework /// <param name="reference">The method reference.</param> public static bool HasMatchingSignature(MethodDefinition definition, MethodReference reference) { - // + // // duplicated by HasMatchingSignature(MethodBase, MethodReference) above // diff --git a/src/SMAPI/Framework/ModLoading/IInstructionHandler.cs b/src/SMAPI/Framework/ModLoading/IInstructionHandler.cs index e6de6785..17c9ba68 100644 --- a/src/SMAPI/Framework/ModLoading/IInstructionHandler.cs +++ b/src/SMAPI/Framework/ModLoading/IInstructionHandler.cs @@ -35,8 +35,7 @@ namespace StardewModdingAPI.Framework.ModLoading /// <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> - bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action<Instruction> replaceWith); + bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction); } } diff --git a/src/SMAPI/Framework/ModLoading/ModFailReason.cs b/src/SMAPI/Framework/ModLoading/ModFailReason.cs new file mode 100644 index 00000000..cd4623e7 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/ModFailReason.cs @@ -0,0 +1,27 @@ +namespace StardewModdingAPI.Framework.ModLoading +{ + /// <summary>Indicates why a mod could not be loaded.</summary> + internal enum ModFailReason + { + /// <summary>The mod has been disabled by prefixing its folder with a dot.</summary> + DisabledByDotConvention, + + /// <summary>Multiple copies of the mod are installed.</summary> + Duplicate, + + /// <summary>The mod has incompatible code instructions, needs a newer SMAPI version, or is marked 'assume broken' in the SMAPI metadata list.</summary> + Incompatible, + + /// <summary>The mod's manifest is missing or invalid.</summary> + InvalidManifest, + + /// <summary>The mod was deemed compatible, but SMAPI failed when it tried to load it.</summary> + LoadFailed, + + /// <summary>The mod requires other mods which aren't installed, or its dependencies have a circular reference.</summary> + MissingDependencies, + + /// <summary>The mod is marked obsolete in the SMAPI metadata list.</summary> + Obsolete + } +} diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index 3ad1bd38..18d2b112 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -16,55 +16,61 @@ namespace StardewModdingAPI.Framework.ModLoading /********* ** Accessors *********/ - /// <summary>The mod's display name.</summary> + /// <inheritdoc /> public string DisplayName { get; } - /// <summary>The root path containing mods.</summary> + /// <inheritdoc /> public string RootPath { get; } - /// <summary>The mod's full directory path within the <see cref="RootPath"/>.</summary> + /// <inheritdoc /> public string DirectoryPath { get; } - /// <summary>The <see cref="DirectoryPath"/> relative to the <see cref="RootPath"/>.</summary> + /// <inheritdoc /> public string RelativeDirectoryPath { get; } - /// <summary>The mod manifest.</summary> + /// <inheritdoc /> public IManifest Manifest { get; } - /// <summary>Metadata about the mod from SMAPI's internal data (if any).</summary> + /// <inheritdoc /> public ModDataRecordVersionedFields DataRecord { get; } - /// <summary>The metadata resolution status.</summary> + /// <inheritdoc /> public ModMetadataStatus Status { get; private set; } - /// <summary>Indicates non-error issues with the mod.</summary> + /// <inheritdoc /> + public ModFailReason? FailReason { get; private set; } + + /// <inheritdoc /> public ModWarning Warnings { get; private set; } - /// <summary>The reason the metadata is invalid, if any.</summary> + /// <inheritdoc /> public string Error { get; private set; } - /// <summary>Whether the mod folder should be ignored. This is <c>true</c> if it was found within a folder whose name starts with a dot.</summary> + /// <inheritdoc /> + public string ErrorDetails { get; private set; } + + /// <inheritdoc /> public bool IsIgnored { get; } - /// <summary>The mod instance (if loaded and <see cref="IsContentPack"/> is false).</summary> + /// <inheritdoc /> public IMod Mod { get; private set; } - /// <summary>The content pack instance (if loaded and <see cref="IsContentPack"/> is true).</summary> + /// <inheritdoc /> public IContentPack ContentPack { get; private set; } - /// <summary>The translations for this mod (if loaded).</summary> + /// <inheritdoc /> public TranslationHelper Translations { get; private set; } - /// <summary>Writes messages to the console and log file as this mod.</summary> + /// <inheritdoc /> public IMonitor Monitor { get; private set; } - /// <summary>The mod-provided API (if any).</summary> + /// <inheritdoc /> public object Api { get; private set; } - /// <summary>The update-check metadata for this mod (if any).</summary> + /// <inheritdoc /> public ModEntryModel UpdateCheckData { get; private set; } - /// <summary>Whether the mod is a content pack.</summary> + /// <inheritdoc /> public bool IsContentPack => this.Manifest?.ContentPackFor != null; @@ -89,28 +95,32 @@ namespace StardewModdingAPI.Framework.ModLoading this.IsIgnored = isIgnored; } - /// <summary>Set the mod status.</summary> - /// <param name="status">The metadata resolution status.</param> - /// <param name="error">The reason the metadata is invalid, if any.</param> - /// <returns>Return the instance for chaining.</returns> - public IModMetadata SetStatus(ModMetadataStatus status, string error = null) + /// <inheritdoc /> + public IModMetadata SetStatusFound() + { + this.SetStatus(ModMetadataStatus.Found, ModFailReason.Incompatible, null); + this.FailReason = null; + return this; + } + + /// <inheritdoc /> + public IModMetadata SetStatus(ModMetadataStatus status, ModFailReason reason, string error, string errorDetails = null) { this.Status = status; + this.FailReason = reason; this.Error = error; + this.ErrorDetails = errorDetails; return this; } - /// <summary>Set a warning flag for the mod.</summary> - /// <param name="warning">The warning to set.</param> + /// <inheritdoc /> public IModMetadata SetWarning(ModWarning warning) { this.Warnings |= warning; return this; } - /// <summary>Set the mod instance.</summary> - /// <param name="mod">The mod instance to set.</param> - /// <param name="translations">The translations for this mod (if loaded).</param> + /// <inheritdoc /> public IModMetadata SetMod(IMod mod, TranslationHelper translations) { if (this.ContentPack != null) @@ -122,10 +132,7 @@ namespace StardewModdingAPI.Framework.ModLoading return this; } - /// <summary>Set the mod instance.</summary> - /// <param name="contentPack">The contentPack instance to set.</param> - /// <param name="monitor">Writes messages to the console and log file.</param> - /// <param name="translations">The translations for this mod (if loaded).</param> + /// <inheritdoc /> public IModMetadata SetMod(IContentPack contentPack, IMonitor monitor, TranslationHelper translations) { if (this.Mod != null) @@ -137,29 +144,27 @@ namespace StardewModdingAPI.Framework.ModLoading return this; } - /// <summary>Set the mod-provided API instance.</summary> - /// <param name="api">The mod-provided API.</param> + /// <inheritdoc /> public IModMetadata SetApi(object api) { this.Api = api; return this; } - /// <summary>Set the update-check metadata for this mod.</summary> - /// <param name="data">The update-check metadata.</param> + /// <inheritdoc /> public IModMetadata SetUpdateData(ModEntryModel data) { this.UpdateCheckData = data; return this; } - /// <summary>Whether the mod manifest was loaded (regardless of whether the mod itself was loaded).</summary> + /// <inheritdoc /> public bool HasManifest() { return this.Manifest != null; } - /// <summary>Whether the mod has an ID (regardless of whether the ID is valid or the mod itself was loaded).</summary> + /// <inheritdoc /> public bool HasID() { return @@ -167,8 +172,7 @@ namespace StardewModdingAPI.Framework.ModLoading && !string.IsNullOrWhiteSpace(this.Manifest.UniqueID); } - /// <summary>Whether the mod has the given ID.</summary> - /// <param name="id">The mod ID to check.</param> + /// <inheritdoc /> public bool HasID(string id) { return @@ -176,8 +180,7 @@ namespace StardewModdingAPI.Framework.ModLoading && string.Equals(this.Manifest.UniqueID.Trim(), id?.Trim(), StringComparison.OrdinalIgnoreCase); } - /// <summary>Get the defined update keys.</summary> - /// <param name="validOnly">Only return valid update keys.</param> + /// <inheritdoc /> public IEnumerable<UpdateKey> GetUpdateKeys(bool validOnly = false) { foreach (string rawKey in this.Manifest?.UpdateKeys ?? new string[0]) @@ -188,8 +191,7 @@ namespace StardewModdingAPI.Framework.ModLoading } } - /// <summary>Get the mod IDs that must be installed to load this mod.</summary> - /// <param name="includeOptional">Whether to include optional dependencies.</param> + /// <inheritdoc /> public IEnumerable<string> GetRequiredModIds(bool includeOptional = false) { HashSet<string> required = new HashSet<string>(StringComparer.OrdinalIgnoreCase); @@ -209,14 +211,13 @@ namespace StardewModdingAPI.Framework.ModLoading yield return this.Manifest.ContentPackFor.UniqueID; } - /// <summary>Whether the mod has at least one valid update key set.</summary> + /// <inheritdoc /> public bool HasValidUpdateKeys() { return this.GetUpdateKeys(validOnly: true).Any(); } - /// <summary>Get whether the mod has any of the given warnings which haven't been suppressed in the <see cref="IModMetadata.DataRecord"/>.</summary> - /// <param name="warnings">The warnings to check.</param> + /// <inheritdoc /> public bool HasUnsuppressedWarnings(params ModWarning[] warnings) { return warnings.Any(warning => @@ -225,7 +226,7 @@ namespace StardewModdingAPI.Framework.ModLoading ); } - /// <summary>Get a relative path which includes the root folder name.</summary> + /// <inheritdoc /> public string GetRelativePathWithRoot() { string rootFolderName = Path.GetFileName(this.RootPath) ?? ""; diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 8bbeb2a3..08df7b76 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -43,8 +43,13 @@ namespace StardewModdingAPI.Framework.ModLoading ? ModMetadataStatus.Found : ModMetadataStatus.Failed; - yield return new ModMetadata(folder.DisplayName, folder.Directory.FullName, rootPath, manifest, dataRecord, isIgnored: shouldIgnore) - .SetStatus(status, shouldIgnore ? "disabled by dot convention" : folder.ManifestParseErrorText); + var metadata = new ModMetadata(folder.DisplayName, folder.Directory.FullName, rootPath, manifest, dataRecord, isIgnored: shouldIgnore); + if (shouldIgnore) + metadata.SetStatus(status, ModFailReason.DisabledByDotConvention, "disabled by dot convention"); + else + metadata.SetStatus(status, ModFailReason.InvalidManifest, folder.ManifestParseErrorText); + + yield return metadata; } } @@ -67,7 +72,7 @@ namespace StardewModdingAPI.Framework.ModLoading switch (mod.DataRecord?.Status) { case ModStatus.Obsolete: - mod.SetStatus(ModMetadataStatus.Failed, $"it's obsolete: {mod.DataRecord.StatusReasonPhrase}"); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.Obsolete, $"it's obsolete: {mod.DataRecord.StatusReasonPhrase}"); continue; case ModStatus.AssumeBroken: @@ -97,7 +102,7 @@ namespace StardewModdingAPI.Framework.ModLoading error += $"version newer than {mod.DataRecord.StatusUpperVersion}"; error += " at " + string.Join(" or ", updateUrls); - mod.SetStatus(ModMetadataStatus.Failed, error); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.Incompatible, error); } continue; } @@ -105,7 +110,7 @@ namespace StardewModdingAPI.Framework.ModLoading // validate SMAPI version if (mod.Manifest.MinimumApiVersion?.IsNewerThan(apiVersion) == true) { - mod.SetStatus(ModMetadataStatus.Failed, $"it needs SMAPI {mod.Manifest.MinimumApiVersion} or later. Please update SMAPI to the latest version to use this mod."); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.Incompatible, $"it needs SMAPI {mod.Manifest.MinimumApiVersion} or later. Please update SMAPI to the latest version to use this mod."); continue; } @@ -117,12 +122,12 @@ namespace StardewModdingAPI.Framework.ModLoading // validate field presence if (!hasDll && !isContentPack) { - mod.SetStatus(ModMetadataStatus.Failed, $"its manifest has no {nameof(IManifest.EntryDll)} or {nameof(IManifest.ContentPackFor)} field; must specify one."); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest has no {nameof(IManifest.EntryDll)} or {nameof(IManifest.ContentPackFor)} field; must specify one."); continue; } if (hasDll && isContentPack) { - mod.SetStatus(ModMetadataStatus.Failed, $"its manifest sets both {nameof(IManifest.EntryDll)} and {nameof(IManifest.ContentPackFor)}, which are mutually exclusive."); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest sets both {nameof(IManifest.EntryDll)} and {nameof(IManifest.ContentPackFor)}, which are mutually exclusive."); continue; } @@ -132,14 +137,14 @@ namespace StardewModdingAPI.Framework.ModLoading // invalid filename format if (mod.Manifest.EntryDll.Intersect(Path.GetInvalidFileNameChars()).Any()) { - mod.SetStatus(ModMetadataStatus.Failed, $"its manifest has invalid filename '{mod.Manifest.EntryDll}' for the EntryDLL field."); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest has invalid filename '{mod.Manifest.EntryDll}' for the EntryDLL field."); continue; } // invalid path if (!File.Exists(Path.Combine(mod.DirectoryPath, mod.Manifest.EntryDll))) { - mod.SetStatus(ModMetadataStatus.Failed, $"its DLL '{mod.Manifest.EntryDll}' doesn't exist."); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its DLL '{mod.Manifest.EntryDll}' doesn't exist."); continue; } @@ -147,7 +152,7 @@ namespace StardewModdingAPI.Framework.ModLoading string actualFilename = new DirectoryInfo(mod.DirectoryPath).GetFiles(mod.Manifest.EntryDll).FirstOrDefault()?.Name; if (actualFilename != mod.Manifest.EntryDll) { - mod.SetStatus(ModMetadataStatus.Failed, $"its {nameof(IManifest.EntryDll)} value '{mod.Manifest.EntryDll}' doesn't match the actual file capitalization '{actualFilename}'. The capitalization must match for crossplatform compatibility."); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its {nameof(IManifest.EntryDll)} value '{mod.Manifest.EntryDll}' doesn't match the actual file capitalization '{actualFilename}'. The capitalization must match for crossplatform compatibility."); continue; } } @@ -158,7 +163,7 @@ namespace StardewModdingAPI.Framework.ModLoading // invalid content pack ID if (string.IsNullOrWhiteSpace(mod.Manifest.ContentPackFor.UniqueID)) { - mod.SetStatus(ModMetadataStatus.Failed, $"its manifest declares {nameof(IManifest.ContentPackFor)} without its required {nameof(IManifestContentPackFor.UniqueID)} field."); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest declares {nameof(IManifest.ContentPackFor)} without its required {nameof(IManifestContentPackFor.UniqueID)} field."); continue; } } @@ -177,14 +182,14 @@ namespace StardewModdingAPI.Framework.ModLoading if (missingFields.Any()) { - mod.SetStatus(ModMetadataStatus.Failed, $"its manifest is missing required fields ({string.Join(", ", missingFields)})."); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest is missing required fields ({string.Join(", ", missingFields)})."); continue; } } // validate ID format if (!PathUtilities.IsSlug(mod.Manifest.UniqueID)) - mod.SetStatus(ModMetadataStatus.Failed, "its manifest specifies an invalid ID (IDs must only contain letters, numbers, underscores, periods, or hyphens)."); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, "its manifest specifies an invalid ID (IDs must only contain letters, numbers, underscores, periods, or hyphens)."); } // validate IDs are unique @@ -199,13 +204,8 @@ namespace StardewModdingAPI.Framework.ModLoading if (mod.Status == ModMetadataStatus.Failed) continue; // don't replace metadata error - string folderList = string.Join(", ", - from entry in @group - let relativePath = entry.GetRelativePathWithRoot() - orderby relativePath - select $"{relativePath} ({entry.Manifest.Version})" - ); - mod.SetStatus(ModMetadataStatus.Failed, $"you have multiple copies of this mod installed. Found in folders: {folderList}."); + string folderList = string.Join(", ", group.Select(p => p.GetRelativePathWithRoot()).OrderBy(p => p)); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.Duplicate, $"you have multiple copies of this mod installed. To fix this, delete these folders and reinstall the mod: {folderList}."); } } } @@ -298,7 +298,7 @@ namespace StardewModdingAPI.Framework.ModLoading if (failedModNames.Any()) { sortedMods.Push(mod); - mod.SetStatus(ModMetadataStatus.Failed, $"it requires mods which aren't installed ({string.Join(", ", failedModNames)})."); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.MissingDependencies, $"it requires mods which aren't installed ({string.Join(", ", failedModNames)})."); return states[mod] = ModDependencyStatus.Failed; } } @@ -315,7 +315,7 @@ namespace StardewModdingAPI.Framework.ModLoading if (failedLabels.Any()) { sortedMods.Push(mod); - mod.SetStatus(ModMetadataStatus.Failed, $"it needs newer versions of some mods: {string.Join(", ", failedLabels)}."); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.MissingDependencies, $"it needs newer versions of some mods: {string.Join(", ", failedLabels)}."); return states[mod] = ModDependencyStatus.Failed; } } @@ -338,7 +338,7 @@ namespace StardewModdingAPI.Framework.ModLoading if (states[requiredMod] == ModDependencyStatus.Checking) { sortedMods.Push(mod); - mod.SetStatus(ModMetadataStatus.Failed, $"its dependencies have a circular reference: {string.Join(" => ", subchain.Select(p => p.DisplayName))} => {requiredMod.DisplayName})."); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.MissingDependencies, $"its dependencies have a circular reference: {string.Join(" => ", subchain.Select(p => p.DisplayName))} => {requiredMod.DisplayName})."); return states[mod] = ModDependencyStatus.Failed; } @@ -354,7 +354,7 @@ namespace StardewModdingAPI.Framework.ModLoading // failed, which means this mod can't be loaded either case ModDependencyStatus.Failed: sortedMods.Push(mod); - mod.SetStatus(ModMetadataStatus.Failed, $"it needs the '{requiredMod.DisplayName}' mod, which couldn't be loaded."); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.MissingDependencies, $"it needs the '{requiredMod.DisplayName}' mod, which couldn't be loaded."); return states[mod] = ModDependencyStatus.Failed; // unexpected status 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 |