summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/StardewModdingAPI.AssemblyRewriters/IMethodRewriter.cs21
-rw-r--r--src/StardewModdingAPI.AssemblyRewriters/PlatformAssemblyMap.cs24
-rw-r--r--src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj1
-rw-r--r--src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj1
-rw-r--r--src/StardewModdingAPI/Constants.cs9
-rw-r--r--src/StardewModdingAPI/Framework/AssemblyRewriting/AssemblyTypeRewriter.cs100
-rw-r--r--src/StardewModdingAPI/StardewModdingAPI.csproj1
7 files changed, 121 insertions, 36 deletions
diff --git a/src/StardewModdingAPI.AssemblyRewriters/IMethodRewriter.cs b/src/StardewModdingAPI.AssemblyRewriters/IMethodRewriter.cs
new file mode 100644
index 00000000..5cbb7e0d
--- /dev/null
+++ b/src/StardewModdingAPI.AssemblyRewriters/IMethodRewriter.cs
@@ -0,0 +1,21 @@
+using Mono.Cecil;
+using Mono.Cecil.Cil;
+
+namespace StardewModdingAPI.AssemblyRewriters
+{
+ /// <summary>Rewrites a method for compatibility.</summary>
+ public interface IMethodRewriter
+ {
+ /// <summary>Get whether the given method reference can be rewritten.</summary>
+ /// <param name="methodRef">The method reference.</param>
+ bool ShouldRewrite(MethodReference methodRef);
+
+ /// <summary>Rewrite a method for compatibility.</summary>
+ /// <param name="module">The module being rewritten.</param>
+ /// <param name="cil">The CIL rewriter.</param>
+ /// <param name="callOp">The instruction which calls the method.</param>
+ /// <param name="methodRef">The method reference invoked by the <paramref name="callOp"/>.</param>
+ /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param>
+ void Rewrite(ModuleDefinition module, ILProcessor cil, Instruction callOp, MethodReference methodRef, PlatformAssemblyMap assemblyMap);
+ }
+}
diff --git a/src/StardewModdingAPI.AssemblyRewriters/PlatformAssemblyMap.cs b/src/StardewModdingAPI.AssemblyRewriters/PlatformAssemblyMap.cs
index c0855719..f2826080 100644
--- a/src/StardewModdingAPI.AssemblyRewriters/PlatformAssemblyMap.cs
+++ b/src/StardewModdingAPI.AssemblyRewriters/PlatformAssemblyMap.cs
@@ -1,4 +1,7 @@
+using System.Collections.Generic;
+using System.Linq;
using System.Reflection;
+using Mono.Cecil;
namespace StardewModdingAPI.AssemblyRewriters
{
@@ -8,6 +11,9 @@ namespace StardewModdingAPI.AssemblyRewriters
/*********
** Accessors
*********/
+ /****
+ ** Data
+ ****/
/// <summary>The target game platform.</summary>
public readonly Platform TargetPlatform;
@@ -15,8 +21,19 @@ namespace StardewModdingAPI.AssemblyRewriters
public readonly string[] RemoveNames;
/// <summary>The assembly filenames to target. Equivalent types should be rewritten to use these assemblies.</summary>
+
+ /****
+ ** Metadata
+ ****/
+ /// <summary>The assemblies to target. Equivalent types should be rewritten to use these assemblies.</summary>
public readonly Assembly[] Targets;
+ /// <summary>An assembly => reference cache.</summary>
+ public readonly IDictionary<Assembly, AssemblyNameReference> TargetReferences;
+
+ /// <summary>An assembly => module cache.</summary>
+ public readonly IDictionary<Assembly, ModuleDefinition> TargetModules;
+
/*********
** Public methods
@@ -27,9 +44,14 @@ namespace StardewModdingAPI.AssemblyRewriters
/// <param name="targetAssemblies">The assemblies to target.</param>
public PlatformAssemblyMap(Platform targetPlatform, string[] removeAssemblyNames, Assembly[] targetAssemblies)
{
+ // save data
this.TargetPlatform = targetPlatform;
this.RemoveNames = removeAssemblyNames;
+
+ // cache assembly metadata
this.Targets = targetAssemblies;
+ this.TargetReferences = this.Targets.ToDictionary(assembly => assembly, assembly => AssemblyNameReference.Parse(assembly.FullName));
+ this.TargetModules = this.Targets.ToDictionary(assembly => assembly, assembly => ModuleDefinition.ReadModule(assembly.Modules.Single().FullyQualifiedName));
}
}
-} \ No newline at end of file
+}
diff --git a/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj b/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj
index d87a48bc..b2533566 100644
--- a/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj
+++ b/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj
@@ -71,6 +71,7 @@
<Compile Include="..\GlobalAssemblyInfo.cs">
<Link>Properties\GlobalAssemblyInfo.cs</Link>
</Compile>
+ <Compile Include="IMethodRewriter.cs" />
<Compile Include="Platform.cs" />
<Compile Include="PlatformAssemblyMap.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
diff --git a/src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj b/src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj
index c19e5f55..8e4d38b3 100644
--- a/src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj
+++ b/src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj
@@ -68,6 +68,7 @@
<!-- copy SMAPI files for Mono -->
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\Newtonsoft.Json.dll" DestinationFolder="$(CompiledInstallerPath)\Mono" />
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\Mono.Cecil.dll" DestinationFolder="$(CompiledInstallerPath)\Mono" />
+ <Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\Mono.Cecil.Rocks.dll" DestinationFolder="$(CompiledInstallerPath)\Mono" />
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.exe" DestinationFolder="$(CompiledInstallerPath)\Mono" />
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.exe.mdb" DestinationFolder="$(CompiledInstallerPath)\Mono" />
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.AssemblyRewriters.dll" DestinationFolder="$(CompiledInstallerPath)\Mono" />
diff --git a/src/StardewModdingAPI/Constants.cs b/src/StardewModdingAPI/Constants.cs
index b9074b6e..4f2f00a1 100644
--- a/src/StardewModdingAPI/Constants.cs
+++ b/src/StardewModdingAPI/Constants.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
@@ -112,6 +113,12 @@ namespace StardewModdingAPI
return new PlatformAssemblyMap(targetPlatform, removeAssemblyReferences, targetAssemblies);
}
+ /// <summary>Get method rewriters which fix incompatible method calls in mod assemblies.</summary>
+ internal static IEnumerable<IMethodRewriter> GetMethodRewriters()
+ {
+ yield break;
+ }
+
/*********
** Private field
@@ -123,4 +130,4 @@ namespace StardewModdingAPI
return $"{prefix}_{Game1.uniqueIDForThisGame}";
}
}
-} \ No newline at end of file
+}
diff --git a/src/StardewModdingAPI/Framework/AssemblyRewriting/AssemblyTypeRewriter.cs b/src/StardewModdingAPI/Framework/AssemblyRewriting/AssemblyTypeRewriter.cs
index 43f6aa11..1e97bdcb 100644
--- a/src/StardewModdingAPI/Framework/AssemblyRewriting/AssemblyTypeRewriter.cs
+++ b/src/StardewModdingAPI/Framework/AssemblyRewriting/AssemblyTypeRewriter.cs
@@ -2,6 +2,8 @@
using System.Linq;
using System.Reflection;
using Mono.Cecil;
+using Mono.Cecil.Cil;
+using Mono.Cecil.Rocks;
using StardewModdingAPI.AssemblyRewriters;
namespace StardewModdingAPI.Framework.AssemblyRewriting
@@ -18,9 +20,6 @@ namespace StardewModdingAPI.Framework.AssemblyRewriting
/// <summary>A type => assembly lookup for types which should be rewritten.</summary>
private readonly IDictionary<string, Assembly> TypeAssemblies;
- /// <summary>An assembly => reference cache.</summary>
- private readonly IDictionary<Assembly, AssemblyNameReference> AssemblyNameReferences;
-
/// <summary>Encapsulates monitoring and logging.</summary>
private readonly IMonitor Monitor;
@@ -37,24 +36,18 @@ namespace StardewModdingAPI.Framework.AssemblyRewriting
this.AssemblyMap = assemblyMap;
this.Monitor = monitor;
- // cache assembly metadata
- this.AssemblyNameReferences = assemblyMap.Targets.ToDictionary(assembly => assembly, assembly => AssemblyNameReference.Parse(assembly.FullName));
-
// collect type => assembly lookup
this.TypeAssemblies = new Dictionary<string, Assembly>();
foreach (Assembly assembly in assemblyMap.Targets)
{
- foreach (Module assemblyModule in assembly.Modules)
+ ModuleDefinition module = this.AssemblyMap.TargetModules[assembly];
+ foreach (TypeDefinition type in module.GetTypes())
{
- ModuleDefinition module = ModuleDefinition.ReadModule(assemblyModule.FullyQualifiedName);
- foreach (TypeDefinition type in module.GetTypes())
- {
- if (!type.IsPublic)
- continue; // no need to rewrite
- if (type.Namespace.Contains("<"))
- continue; // ignore assembly metadata
- this.TypeAssemblies[type.FullName] = assembly;
- }
+ if (!type.IsPublic)
+ continue; // no need to rewrite
+ if (type.Namespace.Contains("<"))
+ continue; // ignore assembly metadata
+ this.TypeAssemblies[type.FullName] = assembly;
}
}
}
@@ -64,13 +57,12 @@ namespace StardewModdingAPI.Framework.AssemblyRewriting
public void RewriteAssembly(AssemblyDefinition assembly)
{
ModuleDefinition module = assembly.Modules.Single(); // technically an assembly can have multiple modules, but none of the build tools (including MSBuild) support it; simplify by assuming one module
- bool shouldRewrite = false;
// remove old assembly references
+ bool shouldRewrite = false;
for (int i = 0; i < module.AssemblyReferences.Count; i++)
{
- bool shouldRemove = this.AssemblyMap.RemoveNames.Any(name => module.AssemblyReferences[i].Name == name);
- if (shouldRemove)
+ if (this.AssemblyMap.RemoveNames.Any(name => module.AssemblyReferences[i].Name == name))
{
this.Monitor.Log($"removing reference to {module.AssemblyReferences[i]}", LogLevel.Trace);
shouldRewrite = true;
@@ -78,25 +70,52 @@ namespace StardewModdingAPI.Framework.AssemblyRewriting
i--;
}
}
+ if (!shouldRewrite)
+ return;
- // replace references
- if (shouldRewrite)
+ // add target assembly references
+ foreach (AssemblyNameReference target in this.AssemblyMap.TargetReferences.Values)
{
- // add target assembly references
- foreach (AssemblyNameReference target in this.AssemblyNameReferences.Values)
- {
- this.Monitor.Log($" adding reference to {target}", LogLevel.Trace);
- module.AssemblyReferences.Add(target);
- }
+ this.Monitor.Log($" adding reference to {target}", LogLevel.Trace);
+ module.AssemblyReferences.Add(target);
+ }
- // rewrite type scopes to use target assemblies
- IEnumerable<TypeReference> typeReferences = module.GetTypeReferences().OrderBy(p => p.FullName);
- string lastTypeLogged = null;
- foreach (TypeReference type in typeReferences)
+ // rewrite type scopes to use target assemblies
+ IEnumerable<TypeReference> typeReferences = module.GetTypeReferences().OrderBy(p => p.FullName);
+ string lastTypeLogged = null;
+ foreach (TypeReference type in typeReferences)
+ {
+ this.ChangeTypeScope(type, shouldLog: type.FullName != lastTypeLogged);
+ lastTypeLogged = type.FullName;
+ }
+
+ // rewrite incompatible methods
+ IMethodRewriter[] methodRewriters = Constants.GetMethodRewriters().ToArray();
+ foreach (MethodDefinition method in this.GetMethods(module))
+ {
+ // skip methods with no rewritable method
+ bool hasMethodToRewrite = method.Body.Instructions.Any(op => (op.OpCode == OpCodes.Call || op.OpCode == OpCodes.Callvirt) && methodRewriters.Any(rewriter => rewriter.ShouldRewrite((MethodReference)op.Operand)));
+ if (!hasMethodToRewrite)
+ continue;
+
+ // rewrite method references
+ method.Body.SimplifyMacros();
+ ILProcessor cil = method.Body.GetILProcessor();
+ Instruction[] instructions = cil.Body.Instructions.ToArray();
+ foreach (Instruction op in instructions)
{
- this.ChangeTypeScope(type, shouldLog: type.FullName != lastTypeLogged);
- lastTypeLogged = type.FullName;
+ if (op.OpCode == OpCodes.Call || op.OpCode == OpCodes.Callvirt)
+ {
+ IMethodRewriter rewriter = methodRewriters.FirstOrDefault(p => p.ShouldRewrite((MethodReference)op.Operand));
+ if (rewriter != null)
+ {
+ MethodReference methodRef = (MethodReference)op.Operand;
+ this.Monitor.Log($"rewriting method reference {methodRef.DeclaringType.FullName}.{methodRef.Name}");
+ rewriter.Rewrite(module, cil, op, methodRef, this.AssemblyMap);
+ }
+ }
}
+ method.Body.OptimizeMacros();
}
}
@@ -119,10 +138,23 @@ namespace StardewModdingAPI.Framework.AssemblyRewriting
return;
// replace scope
- AssemblyNameReference assemblyRef = this.AssemblyNameReferences[assembly];
+ AssemblyNameReference assemblyRef = this.AssemblyMap.TargetReferences[assembly];
if (shouldLog)
this.Monitor.Log($"redirecting {type.FullName} from {type.Scope.Name} to {assemblyRef.Name}", LogLevel.Trace);
type.Scope = assemblyRef;
}
+
+ /// <summary>Get all methods in a module.</summary>
+ /// <param name="module">The module to search.</param>
+ private IEnumerable<MethodDefinition> GetMethods(ModuleDefinition module)
+ {
+ return (
+ from type in module.GetTypes()
+ where type.HasMethods
+ from method in type.Methods
+ where method.HasBody
+ select method
+ );
+ }
}
}
diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj
index 6f3bcb4b..f57c1b6f 100644
--- a/src/StardewModdingAPI/StardewModdingAPI.csproj
+++ b/src/StardewModdingAPI/StardewModdingAPI.csproj
@@ -252,5 +252,6 @@
<Copy SourceFiles="$(TargetDir)\$(TargetName).xml" DestinationFolder="$(GamePath)" />
<Copy SourceFiles="$(TargetDir)\Newtonsoft.Json.dll" DestinationFolder="$(GamePath)" />
<Copy SourceFiles="$(TargetDir)\Mono.Cecil.dll" DestinationFolder="$(GamePath)" />
+ <Copy SourceFiles="$(TargetDir)\Mono.Cecil.Rocks.dll" DestinationFolder="$(GamePath)" />
</Target>
</Project> \ No newline at end of file