summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--build/common.targets1
-rw-r--r--build/prepare-install-package.targets2
-rw-r--r--src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs11
-rw-r--r--src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs138
-rw-r--r--src/SMAPI/Program.cs3
-rw-r--r--src/SMAPI/StardewModdingAPI.csproj4
-rw-r--r--src/SMAPI/packages.config1
7 files changed, 149 insertions, 11 deletions
diff --git a/build/common.targets b/build/common.targets
index 15c935e3..aa11344e 100644
--- a/build/common.targets
+++ b/build/common.targets
@@ -86,7 +86,6 @@
<Copy SourceFiles="$(TargetDir)\StardewModdingAPI.AssemblyRewriters.dll" DestinationFolder="$(GamePath)" />
<Copy SourceFiles="$(TargetDir)\$(TargetName).pdb" DestinationFolder="$(GamePath)" />
<Copy SourceFiles="$(TargetDir)\$(TargetName).xml" DestinationFolder="$(GamePath)" />
- <Copy SourceFiles="$(TargetDir)\ImpromptuInterface.dll" DestinationFolder="$(GamePath)" />
<Copy SourceFiles="$(TargetDir)\Newtonsoft.Json.dll" DestinationFolder="$(GamePath)" />
<Copy SourceFiles="$(TargetDir)\Mono.Cecil.dll" DestinationFolder="$(GamePath)" />
</Target>
diff --git a/build/prepare-install-package.targets b/build/prepare-install-package.targets
index f8262271..dde2ff0a 100644
--- a/build/prepare-install-package.targets
+++ b/build/prepare-install-package.targets
@@ -22,7 +22,6 @@
<Copy SourceFiles="$(TargetDir)\readme.txt" DestinationFiles="$(PackagePath)\README.txt" />
<!-- copy SMAPI files for Mono -->
- <Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\ImpromptuInterface.dll" DestinationFolder="$(PackageInternalPath)\Mono" />
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\Mono.Cecil.dll" DestinationFolder="$(PackageInternalPath)\Mono" />
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\Mono.CSharp.dll" DestinationFolder="$(PackageInternalPath)\Mono" />
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\Newtonsoft.Json.dll" DestinationFolder="$(PackageInternalPath)\Mono" />
@@ -38,7 +37,6 @@
<Copy Condition="$(OS) != 'Windows_NT'" SourceFiles="@(CompiledMods)" DestinationFolder="$(PackageInternalPath)\Mono\Mods\%(RecursiveDir)" />
<!-- copy SMAPI files for Windows -->
- <Copy Condition="$(OS) == 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\ImpromptuInterface.dll" DestinationFolder="$(PackageInternalPath)\Windows" />
<Copy Condition="$(OS) == 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\Mono.Cecil.dll" DestinationFolder="$(PackageInternalPath)\Windows" />
<Copy Condition="$(OS) == 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\Newtonsoft.Json.dll" DestinationFolder="$(PackageInternalPath)\Windows" />
<Copy Condition="$(OS) == 'Windows_NT'" SourceFiles="$(CompiledSmapiPath)\StardewModdingAPI.exe" DestinationFolder="$(PackageInternalPath)\Windows" />
diff --git a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs
index 9574a632..ea0dbb38 100644
--- a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs
@@ -1,6 +1,6 @@
using System.Collections.Generic;
using System.Linq;
-using ImpromptuInterface;
+using StardewModdingAPI.Framework.Reflection;
namespace StardewModdingAPI.Framework.ModHelpers
{
@@ -19,6 +19,9 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <summary>The mod IDs for APIs accessed by this instanced.</summary>
private readonly HashSet<string> AccessedModApis = new HashSet<string>();
+ /// <summary>Generates proxy classes to access mod APIs through an arbitrary interface.</summary>
+ private readonly InterfaceProxyBuilder ProxyBuilder;
+
/*********
** Public methods
@@ -26,11 +29,13 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <summary>Construct an instance.</summary>
/// <param name="modID">The unique ID of the relevant mod.</param>
/// <param name="registry">The underlying mod registry.</param>
+ /// <param name="proxyBuilder">Generates proxy classes to access mod APIs through an arbitrary interface.</param>
/// <param name="monitor">Encapsulates monitoring and logging for the mod.</param>
- public ModRegistryHelper(string modID, ModRegistry registry, IMonitor monitor)
+ public ModRegistryHelper(string modID, ModRegistry registry, InterfaceProxyBuilder proxyBuilder, IMonitor monitor)
: base(modID)
{
this.Registry = registry;
+ this.ProxyBuilder = proxyBuilder;
this.Monitor = monitor;
}
@@ -94,7 +99,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
// get API of type
if (api is TInterface castApi)
return castApi;
- return api.ActLike<TInterface>();
+ return this.ProxyBuilder.CreateProxy<TInterface>(api, this.ModID, uniqueID);
}
}
}
diff --git a/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs b/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs
new file mode 100644
index 00000000..5abebc18
--- /dev/null
+++ b/src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs
@@ -0,0 +1,138 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Reflection.Emit;
+
+namespace StardewModdingAPI.Framework.Reflection
+{
+ /// <summary>Generates proxy classes to access mod APIs through an arbitrary interface.</summary>
+ internal class InterfaceProxyBuilder
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>The CLR module in which to create proxy classes.</summary>
+ private readonly ModuleBuilder ModuleBuilder;
+
+ /// <summary>The generated proxy types.</summary>
+ private readonly IDictionary<string, Type> GeneratedTypes = new Dictionary<string, Type>();
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ public InterfaceProxyBuilder()
+ {
+ AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName($"StardewModdingAPI.Proxies, Version={this.GetType().Assembly.GetName().Version}, Culture=neutral"), AssemblyBuilderAccess.Run);
+ this.ModuleBuilder = assemblyBuilder.DefineDynamicModule("StardewModdingAPI.Proxies");
+ }
+
+ /// <summary>Create an API proxy.</summary>
+ /// <typeparam name="TInterface">The interface through which to access the API.</typeparam>
+ /// <param name="instance">The API instance to access.</param>
+ /// <param name="sourceModID">The unique ID of the mod consuming the API.</param>
+ /// <param name="targetModID">The unique ID of the mod providing the API.</param>
+ public TInterface CreateProxy<TInterface>(object instance, string sourceModID, string targetModID)
+ where TInterface : class
+ {
+ // validate
+ if (instance == null)
+ throw new InvalidOperationException("Can't proxy access to a null API.");
+ if (!typeof(TInterface).IsInterface)
+ throw new InvalidOperationException("The proxy type must be an interface, not a class.");
+
+ // get proxy type
+ Type targetType = instance.GetType();
+ string proxyTypeName = $"StardewModdingAPI.Proxies.From<{sourceModID}_{typeof(TInterface).FullName}>_To<{targetModID}_{targetType.FullName}>";
+ if (!this.GeneratedTypes.TryGetValue(proxyTypeName, out Type type))
+ {
+ type = this.CreateProxyType(proxyTypeName, typeof(TInterface), targetType);
+ this.GeneratedTypes[proxyTypeName] = type;
+ }
+
+ // create instance
+ ConstructorInfo constructor = type.GetConstructor(new[] { targetType });
+ if (constructor == null)
+ throw new InvalidOperationException($"Couldn't find the constructor for generated proxy type '{proxyTypeName}'."); // should never happen
+ return (TInterface)constructor.Invoke(new[] { instance });
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Define a class which proxies access to a target type through an interface.</summary>
+ /// <param name="proxyTypeName">The name of the proxy type to generate.</param>
+ /// <param name="interfaceType">The interface type through which to access the target.</param>
+ /// <param name="targetType">The target type to access.</param>
+ private Type CreateProxyType(string proxyTypeName, Type interfaceType, Type targetType)
+ {
+ // define proxy type
+ TypeBuilder proxyBuilder = this.ModuleBuilder.DefineType(proxyTypeName, TypeAttributes.Public | TypeAttributes.Class);
+ proxyBuilder.AddInterfaceImplementation(interfaceType);
+
+ // create field to store target instance
+ FieldBuilder field = proxyBuilder.DefineField("__Target", targetType, FieldAttributes.Private);
+
+ // create constructor which accepts target instance
+ {
+ ConstructorBuilder constructor = proxyBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard | CallingConventions.HasThis, new[] { targetType });
+ ILGenerator il = constructor.GetILGenerator();
+
+ il.Emit(OpCodes.Ldarg_0); // this
+ // ReSharper disable once AssignNullToNotNullAttribute -- never null
+ il.Emit(OpCodes.Call, typeof(object).GetConstructor(new Type[0])); // call base constructor
+ il.Emit(OpCodes.Ldarg_0); // this
+ il.Emit(OpCodes.Ldarg_1); // load argument
+ il.Emit(OpCodes.Stfld, field); // set field to loaded argument
+ il.Emit(OpCodes.Ret);
+ }
+
+ // proxy methods
+ foreach (MethodInfo proxyMethod in interfaceType.GetMethods())
+ {
+ var targetMethod = targetType.GetMethod(proxyMethod.Name, proxyMethod.GetParameters().Select(a => a.ParameterType).ToArray());
+ if (targetMethod == null)
+ throw new InvalidOperationException($"The {interfaceType.FullName} interface defines method {proxyMethod.Name} which doesn't exist in the API.");
+
+ this.ProxyMethod(proxyBuilder, targetMethod, field);
+ }
+
+ // create type
+ return proxyBuilder.CreateType();
+ }
+
+ /// <summary>Define a method which proxies access to a method on the target.</summary>
+ /// <param name="proxyBuilder">The proxy type being generated.</param>
+ /// <param name="target">The target method.</param>
+ /// <param name="instanceField">The proxy field containing the API instance.</param>
+ private void ProxyMethod(TypeBuilder proxyBuilder, MethodInfo target, FieldBuilder instanceField)
+ {
+ Type[] argTypes = target.GetParameters().Select(a => a.ParameterType).ToArray();
+
+ // create method
+ MethodBuilder methodBuilder = proxyBuilder.DefineMethod(target.Name, MethodAttributes.Public | MethodAttributes.Final | MethodAttributes.Virtual);
+ methodBuilder.SetParameters(argTypes);
+ methodBuilder.SetReturnType(target.ReturnType);
+
+ // create method body
+ {
+ ILGenerator il = methodBuilder.GetILGenerator();
+
+ // load target instance
+ il.Emit(OpCodes.Ldarg_0);
+ il.Emit(OpCodes.Ldfld, instanceField);
+
+ // invoke target method on instance
+ for (int i = 0; i < argTypes.Length; i++)
+ il.Emit(OpCodes.Ldarg, i + 1);
+ il.Emit(OpCodes.Call, target);
+
+ // return result
+ il.Emit(OpCodes.Ret);
+ }
+ }
+ }
+}
diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs
index 786549fe..7eda9c66 100644
--- a/src/SMAPI/Program.cs
+++ b/src/SMAPI/Program.cs
@@ -655,6 +655,7 @@ namespace StardewModdingAPI
AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.TargetPlatform, this.Monitor, this.Settings.DeveloperMode);
AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => modAssemblyLoader.ResolveAssembly(e.Name);
+ InterfaceProxyBuilder proxyBuilder = new InterfaceProxyBuilder();
foreach (IModMetadata metadata in mods)
{
// get basic info
@@ -706,7 +707,7 @@ namespace StardewModdingAPI
ICommandHelper commandHelper = new CommandHelper(manifest.UniqueID, metadata.DisplayName, this.CommandManager);
IContentHelper contentHelper = new ContentHelper(contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor);
IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, metadata.DisplayName, this.Reflection, this.DeprecationManager);
- IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry, monitor);
+ IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry, proxyBuilder, monitor);
ITranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentManager.GetLocale(), contentManager.GetCurrentLanguage());
modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, translationHelper);
}
diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj
index 026ac106..0e8ccaa3 100644
--- a/src/SMAPI/StardewModdingAPI.csproj
+++ b/src/SMAPI/StardewModdingAPI.csproj
@@ -53,9 +53,6 @@
<ApplicationIcon>icon.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
- <Reference Include="ImpromptuInterface, Version=6.2.2.0, Culture=neutral, PublicKeyToken=0b1781c923b2975b, processorArchitecture=MSIL">
- <HintPath>..\packages\ImpromptuInterface.6.2.2\lib\net40\ImpromptuInterface.dll</HintPath>
- </Reference>
<Reference Include="Mono.Cecil, Version=0.9.6.0, Culture=neutral, PublicKeyToken=0738eb9f132ed756, processorArchitecture=MSIL">
<HintPath>..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.dll</HintPath>
<Private>True</Private>
@@ -116,6 +113,7 @@
<Compile Include="Framework\ContentManagerShim.cs" />
<Compile Include="Framework\Exceptions\SAssemblyLoadFailedException.cs" />
<Compile Include="Framework\ModLoading\AssemblyLoadStatus.cs" />
+ <Compile Include="Framework\Reflection\InterfaceProxyBuilder.cs" />
<Compile Include="Framework\Utilities\ContextHash.cs" />
<Compile Include="IReflectedField.cs" />
<Compile Include="IReflectedMethod.cs" />
diff --git a/src/SMAPI/packages.config b/src/SMAPI/packages.config
index 60be6881..98d742c7 100644
--- a/src/SMAPI/packages.config
+++ b/src/SMAPI/packages.config
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
- <package id="ImpromptuInterface" version="6.2.2" targetFramework="net45" />
<package id="Mono.Cecil" version="0.9.6.4" targetFramework="net45" />
<package id="Newtonsoft.Json" version="10.0.3" targetFramework="net45" />
</packages> \ No newline at end of file