diff options
-rw-r--r-- | build/common.targets | 1 | ||||
-rw-r--r-- | build/prepare-install-package.targets | 2 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs | 11 | ||||
-rw-r--r-- | src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs | 138 | ||||
-rw-r--r-- | src/SMAPI/Program.cs | 3 | ||||
-rw-r--r-- | src/SMAPI/StardewModdingAPI.csproj | 4 | ||||
-rw-r--r-- | src/SMAPI/packages.config | 1 |
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 |