summaryrefslogtreecommitdiff
path: root/src/SMAPI/Framework/ModLoading
diff options
context:
space:
mode:
Diffstat (limited to 'src/SMAPI/Framework/ModLoading')
-rw-r--r--src/SMAPI/Framework/ModLoading/AssemblyLoader.cs31
-rw-r--r--src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs10
-rw-r--r--src/SMAPI/Framework/ModLoading/Finders/FieldFinder.cs10
-rw-r--r--src/SMAPI/Framework/ModLoading/Finders/MethodFinder.cs10
-rw-r--r--src/SMAPI/Framework/ModLoading/Finders/PropertyFinder.cs10
-rw-r--r--src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs10
-rw-r--r--src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs17
-rw-r--r--src/SMAPI/Framework/ModLoading/Finders/TypeAssemblyFinder.cs6
-rw-r--r--src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs6
-rw-r--r--src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs23
-rw-r--r--src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs67
-rw-r--r--src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs24
-rw-r--r--src/SMAPI/Framework/ModLoading/IInstructionHandler.cs3
-rw-r--r--src/SMAPI/Framework/ModLoading/ModFailReason.cs27
-rw-r--r--src/SMAPI/Framework/ModLoading/ModMetadata.cs95
-rw-r--r--src/SMAPI/Framework/ModLoading/ModResolver.cs48
-rw-r--r--src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs35
-rw-r--r--src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs65
-rw-r--r--src/SMAPI/Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs13
-rw-r--r--src/SMAPI/Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs106
-rw-r--r--src/SMAPI/Framework/ModLoading/Rewriters/HeuristicMethodRewriter.cs109
-rw-r--r--src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs9
-rw-r--r--src/SMAPI/Framework/ModLoading/Rewriters/StaticFieldToConstantRewriter.cs74
-rw-r--r--src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs6
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