move proxying to a separate NuGet library
This commit is contained in:
parent
a54d58d064
commit
ba42bb97d1
|
@ -53,6 +53,7 @@
|
||||||
<Copy SourceFiles="$(TargetDir)\SMAPI.metadata.json" DestinationFiles="$(GamePath)\smapi-internal\metadata.json" />
|
<Copy SourceFiles="$(TargetDir)\SMAPI.metadata.json" DestinationFiles="$(GamePath)\smapi-internal\metadata.json" />
|
||||||
<Copy SourceFiles="$(TargetDir)\Newtonsoft.Json.dll" DestinationFolder="$(GamePath)\smapi-internal" />
|
<Copy SourceFiles="$(TargetDir)\Newtonsoft.Json.dll" DestinationFolder="$(GamePath)\smapi-internal" />
|
||||||
<Copy SourceFiles="$(TargetDir)\TMXTile.dll" DestinationFolder="$(GamePath)\smapi-internal" />
|
<Copy SourceFiles="$(TargetDir)\TMXTile.dll" DestinationFolder="$(GamePath)\smapi-internal" />
|
||||||
|
<Copy SourceFiles="$(TargetDir)\Pintail.dll" DestinationFolder="$(GamePath)\smapi-internal" />
|
||||||
<Copy SourceFiles="@(TranslationFiles)" DestinationFolder="$(GamePath)\smapi-internal\i18n" />
|
<Copy SourceFiles="@(TranslationFiles)" DestinationFolder="$(GamePath)\smapi-internal\i18n" />
|
||||||
|
|
||||||
<!-- Harmony + dependencies -->
|
<!-- Harmony + dependencies -->
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
<PackageReference Include="Moq" Version="4.16.1" />
|
<PackageReference Include="Moq" Version="4.16.1" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||||
|
<PackageReference Include="Pintail" Version="1.2.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using Nanoray.Pintail;
|
||||||
using StardewModdingAPI.Framework.Reflection;
|
using StardewModdingAPI.Framework.Reflection;
|
||||||
|
|
||||||
namespace StardewModdingAPI.Framework.ModHelpers
|
namespace StardewModdingAPI.Framework.ModHelpers
|
||||||
|
@ -19,7 +20,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||||
private readonly HashSet<string> AccessedModApis = new HashSet<string>();
|
private readonly HashSet<string> AccessedModApis = new HashSet<string>();
|
||||||
|
|
||||||
/// <summary>Generates proxy classes to access mod APIs through an arbitrary interface.</summary>
|
/// <summary>Generates proxy classes to access mod APIs through an arbitrary interface.</summary>
|
||||||
private readonly InterfaceProxyFactory ProxyFactory;
|
private readonly IProxyManager<string> ProxyManager;
|
||||||
|
|
||||||
|
|
||||||
/*********
|
/*********
|
||||||
|
@ -28,13 +29,13 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||||
/// <summary>Construct an instance.</summary>
|
/// <summary>Construct an instance.</summary>
|
||||||
/// <param name="modID">The unique ID of the relevant mod.</param>
|
/// <param name="modID">The unique ID of the relevant mod.</param>
|
||||||
/// <param name="registry">The underlying mod registry.</param>
|
/// <param name="registry">The underlying mod registry.</param>
|
||||||
/// <param name="proxyFactory">Generates proxy classes to access mod APIs through an arbitrary interface.</param>
|
/// <param name="proxyManager">Generates proxy classes to access mod APIs through an arbitrary interface.</param>
|
||||||
/// <param name="monitor">Encapsulates monitoring and logging for the mod.</param>
|
/// <param name="monitor">Encapsulates monitoring and logging for the mod.</param>
|
||||||
public ModRegistryHelper(string modID, ModRegistry registry, InterfaceProxyFactory proxyFactory, IMonitor monitor)
|
public ModRegistryHelper(string modID, ModRegistry registry, IProxyManager<string> proxyManager, IMonitor monitor)
|
||||||
: base(modID)
|
: base(modID)
|
||||||
{
|
{
|
||||||
this.Registry = registry;
|
this.Registry = registry;
|
||||||
this.ProxyFactory = proxyFactory;
|
this.ProxyManager = proxyManager;
|
||||||
this.Monitor = monitor;
|
this.Monitor = monitor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,33 +97,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||||
// get API of type
|
// get API of type
|
||||||
if (api is TInterface castApi)
|
if (api is TInterface castApi)
|
||||||
return castApi;
|
return castApi;
|
||||||
return this.ProxyFactory.CreateProxy<TInterface>(api, this.ModID, uniqueID);
|
return this.ProxyManager.ObtainProxy<string, TInterface>(api, this.ModID, uniqueID);
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public bool TryProxy<TInterface>(string uniqueID, object toProxy, out TInterface proxy) where TInterface : class
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
foreach (var toProxyInterface in toProxy.GetType().GetInterfaces())
|
|
||||||
{
|
|
||||||
var unproxyBuilder = this.ProxyFactory.ObtainBuilder(typeof(TInterface), toProxyInterface, this.ModID, uniqueID);
|
|
||||||
if (unproxyBuilder.TryUnproxy(toProxy, out object targetInstance))
|
|
||||||
{
|
|
||||||
proxy = (TInterface)targetInstance;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var proxyBuilder = this.ProxyFactory.ObtainBuilder(toProxy.GetType(), typeof(TInterface), this.ModID, uniqueID);
|
|
||||||
proxy = (TInterface)proxyBuilder.ObtainInstance(toProxy, this.ProxyFactory);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
proxy = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,405 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Reflection.Emit;
|
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
|
|
||||||
namespace StardewModdingAPI.Framework.Reflection
|
|
||||||
{
|
|
||||||
/// <summary>Generates a proxy class to access a mod API through an arbitrary interface.</summary>
|
|
||||||
internal class InterfaceProxyBuilder
|
|
||||||
{
|
|
||||||
/*********
|
|
||||||
** Consts
|
|
||||||
*********/
|
|
||||||
private static readonly string TargetFieldName = "__Target";
|
|
||||||
private static readonly string GlueFieldName = "__Glue";
|
|
||||||
private static readonly MethodInfo ObtainInstanceForProxyTypeNameMethod = typeof(InterfaceProxyGlue).GetMethod(nameof(InterfaceProxyGlue.ObtainInstanceForProxyTypeName), new Type[] { typeof(string), typeof(object) });
|
|
||||||
private static readonly MethodInfo UnproxyOrObtainInstanceForProxyTypeNameMethod = typeof(InterfaceProxyGlue).GetMethod(nameof(InterfaceProxyGlue.UnproxyOrObtainInstanceForProxyTypeName), new Type[] { typeof(string), typeof(string), typeof(object) });
|
|
||||||
|
|
||||||
/*********
|
|
||||||
** Fields
|
|
||||||
*********/
|
|
||||||
/// <summary>The target class type.</summary>
|
|
||||||
private readonly Type TargetType;
|
|
||||||
|
|
||||||
/// <summary>The interfce type.</summary>
|
|
||||||
private readonly Type InterfaceType;
|
|
||||||
|
|
||||||
/// <summary>The full name of the generated proxy type.</summary>
|
|
||||||
private readonly string ProxyTypeName;
|
|
||||||
|
|
||||||
/// <summary>The generated proxy type.</summary>
|
|
||||||
private Type ProxyType;
|
|
||||||
|
|
||||||
/// <summary>A cache of all proxies generated by this builder.</summary>
|
|
||||||
private readonly ConditionalWeakTable<object, object> ProxyCache = new();
|
|
||||||
|
|
||||||
/*********
|
|
||||||
** Public methods
|
|
||||||
*********/
|
|
||||||
/// <summary>Construct an instance.</summary>
|
|
||||||
/// <param name="targetType">The target type.</param>
|
|
||||||
/// <param name="interfaceType">The interface type to implement.</param>
|
|
||||||
/// <param name="proxyTypeName">The type name to generate.</param>
|
|
||||||
public InterfaceProxyBuilder(Type targetType, Type interfaceType, string proxyTypeName)
|
|
||||||
{
|
|
||||||
// validate and store
|
|
||||||
this.TargetType = targetType ?? throw new ArgumentNullException(nameof(targetType));
|
|
||||||
this.InterfaceType = interfaceType ?? throw new ArgumentNullException(nameof(interfaceType));
|
|
||||||
this.ProxyTypeName = proxyTypeName ?? throw new ArgumentNullException(nameof(proxyTypeName));
|
|
||||||
if (!interfaceType.IsInterface)
|
|
||||||
throw new ArgumentException($"{nameof(interfaceType)} is not an interface.");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>Creates and sets up the proxy type.</summary>
|
|
||||||
/// <param name="factory">The <see cref="InterfaceProxyFactory"/> that requested to build a proxy.</param>
|
|
||||||
/// <param name="moduleBuilder">The CLR module in which to create proxy classes.</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 void SetupProxyType(InterfaceProxyFactory factory, ModuleBuilder moduleBuilder, string sourceModID, string targetModID)
|
|
||||||
{
|
|
||||||
// define proxy type
|
|
||||||
TypeBuilder proxyBuilder = moduleBuilder.DefineType(this.ProxyTypeName, TypeAttributes.Public | TypeAttributes.Class);
|
|
||||||
proxyBuilder.AddInterfaceImplementation(this.InterfaceType);
|
|
||||||
|
|
||||||
// create fields to store target instance and proxy factory
|
|
||||||
FieldBuilder targetField = proxyBuilder.DefineField(TargetFieldName, this.TargetType, FieldAttributes.Private);
|
|
||||||
FieldBuilder glueField = proxyBuilder.DefineField(GlueFieldName, typeof(InterfaceProxyGlue), FieldAttributes.Private);
|
|
||||||
|
|
||||||
// create constructor which accepts target instance + factory, and sets fields
|
|
||||||
{
|
|
||||||
ConstructorBuilder constructor = proxyBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard | CallingConventions.HasThis, new[] { this.TargetType, typeof(InterfaceProxyGlue) });
|
|
||||||
ILGenerator il = constructor.GetILGenerator();
|
|
||||||
|
|
||||||
il.Emit(OpCodes.Ldarg_0); // this
|
|
||||||
// ReSharper disable once AssignNullToNotNullAttribute -- never null
|
|
||||||
il.Emit(OpCodes.Call, typeof(object).GetConstructor(Array.Empty<Type>())); // call base constructor
|
|
||||||
il.Emit(OpCodes.Ldarg_0); // this
|
|
||||||
il.Emit(OpCodes.Ldarg_1); // load argument
|
|
||||||
il.Emit(OpCodes.Stfld, targetField); // set field to loaded argument
|
|
||||||
il.Emit(OpCodes.Ldarg_0); // this
|
|
||||||
il.Emit(OpCodes.Ldarg_2); // load argument
|
|
||||||
il.Emit(OpCodes.Stfld, glueField); // set field to loaded argument
|
|
||||||
il.Emit(OpCodes.Ret);
|
|
||||||
}
|
|
||||||
|
|
||||||
var allTargetMethods = this.TargetType.GetMethods().ToList();
|
|
||||||
foreach (Type targetInterface in this.TargetType.GetInterfaces())
|
|
||||||
{
|
|
||||||
foreach (MethodInfo targetMethod in targetInterface.GetMethods())
|
|
||||||
{
|
|
||||||
if (!targetMethod.IsAbstract)
|
|
||||||
allTargetMethods.Add(targetMethod);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MatchingTypesResult AreTypesMatching(Type targetType, Type proxyType, MethodTypeMatchingPart part)
|
|
||||||
{
|
|
||||||
var typeA = part == MethodTypeMatchingPart.Parameter ? targetType : proxyType;
|
|
||||||
var typeB = part == MethodTypeMatchingPart.Parameter ? proxyType : targetType;
|
|
||||||
|
|
||||||
if (typeA.IsGenericMethodParameter != typeB.IsGenericMethodParameter)
|
|
||||||
return MatchingTypesResult.False;
|
|
||||||
// TODO: decide if "assignable" checking is desired (instead of just 1:1 type equality)
|
|
||||||
if (typeA.IsGenericMethodParameter ? typeA.GenericParameterPosition == typeB.GenericParameterPosition : typeA.IsAssignableFrom(typeB))
|
|
||||||
return MatchingTypesResult.True;
|
|
||||||
|
|
||||||
if (!proxyType.IsGenericMethodParameter)
|
|
||||||
{
|
|
||||||
if (proxyType.GetNonRefType().IsInterface)
|
|
||||||
return MatchingTypesResult.IfProxied;
|
|
||||||
if (targetType.GetNonRefType().IsInterface)
|
|
||||||
return MatchingTypesResult.IfProxied;
|
|
||||||
}
|
|
||||||
|
|
||||||
return MatchingTypesResult.False;
|
|
||||||
}
|
|
||||||
|
|
||||||
// proxy methods
|
|
||||||
foreach (MethodInfo proxyMethod in this.InterfaceType.GetMethods())
|
|
||||||
{
|
|
||||||
var proxyMethodParameters = proxyMethod.GetParameters();
|
|
||||||
var proxyMethodGenericArguments = proxyMethod.GetGenericArguments();
|
|
||||||
|
|
||||||
foreach (MethodInfo targetMethod in allTargetMethods)
|
|
||||||
{
|
|
||||||
// checking if `targetMethod` matches `proxyMethod`
|
|
||||||
|
|
||||||
if (targetMethod.Name != proxyMethod.Name)
|
|
||||||
continue;
|
|
||||||
if (targetMethod.GetGenericArguments().Length != proxyMethodGenericArguments.Length)
|
|
||||||
continue;
|
|
||||||
var positionsToProxy = new HashSet<int?>(); // null = return type; anything else = parameter position
|
|
||||||
|
|
||||||
switch (AreTypesMatching(targetMethod.ReturnType, proxyMethod.ReturnType, MethodTypeMatchingPart.ReturnType))
|
|
||||||
{
|
|
||||||
case MatchingTypesResult.False:
|
|
||||||
continue;
|
|
||||||
case MatchingTypesResult.True:
|
|
||||||
break;
|
|
||||||
case MatchingTypesResult.IfProxied:
|
|
||||||
positionsToProxy.Add(null);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
var mParameters = targetMethod.GetParameters();
|
|
||||||
if (mParameters.Length != proxyMethodParameters.Length)
|
|
||||||
continue;
|
|
||||||
for (int i = 0; i < mParameters.Length; i++)
|
|
||||||
{
|
|
||||||
switch (AreTypesMatching(mParameters[i].ParameterType, proxyMethodParameters[i].ParameterType, MethodTypeMatchingPart.Parameter))
|
|
||||||
{
|
|
||||||
case MatchingTypesResult.False:
|
|
||||||
goto targetMethodLoopContinue;
|
|
||||||
case MatchingTypesResult.True:
|
|
||||||
break;
|
|
||||||
case MatchingTypesResult.IfProxied:
|
|
||||||
positionsToProxy.Add(i);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// method matched; proxying
|
|
||||||
|
|
||||||
this.ProxyMethod(factory, proxyBuilder, proxyMethod, targetMethod, targetField, glueField, positionsToProxy, sourceModID, targetModID);
|
|
||||||
goto proxyMethodLoopContinue;
|
|
||||||
targetMethodLoopContinue:;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new InvalidOperationException($"The {this.InterfaceType.FullName} interface defines method {proxyMethod.Name} which doesn't exist in the API.");
|
|
||||||
proxyMethodLoopContinue:;
|
|
||||||
}
|
|
||||||
|
|
||||||
// save info
|
|
||||||
this.ProxyType = proxyBuilder.CreateType();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Get an existing or create a new instance of the proxy for a target instance.</summary>
|
|
||||||
/// <param name="targetInstance">The target instance.</param>
|
|
||||||
/// <param name="factory">The <see cref="InterfaceProxyFactory"/> that requested to build a proxy.</param>
|
|
||||||
public object ObtainInstance(object targetInstance, InterfaceProxyFactory factory)
|
|
||||||
{
|
|
||||||
if (this.ProxyCache.TryGetValue(targetInstance, out object proxyInstance))
|
|
||||||
return proxyInstance;
|
|
||||||
|
|
||||||
ConstructorInfo constructor = this.ProxyType.GetConstructor(new[] { this.TargetType, typeof(InterfaceProxyGlue) });
|
|
||||||
if (constructor == null)
|
|
||||||
throw new InvalidOperationException($"Couldn't find the constructor for generated proxy type '{this.ProxyType.Name}'."); // should never happen
|
|
||||||
proxyInstance = constructor.Invoke(new[] { targetInstance, new InterfaceProxyGlue(factory) });
|
|
||||||
this.ProxyCache.Add(targetInstance, proxyInstance);
|
|
||||||
return proxyInstance;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*********
|
|
||||||
** Private methods
|
|
||||||
*********/
|
|
||||||
/// <summary>Define a method which proxies access to a method on the target.</summary>
|
|
||||||
/// <param name="factory">The <see cref="InterfaceProxyFactory"/> that requested to build a proxy.</param>
|
|
||||||
/// <param name="proxyBuilder">The proxy type being generated.</param>
|
|
||||||
/// <param name="proxy">The proxy method.</param>
|
|
||||||
/// <param name="target">The target method.</param>
|
|
||||||
/// <param name="instanceField">The proxy field containing the API instance.</param>
|
|
||||||
/// <param name="glueField">The proxy field containing an <see cref="InterfaceProxyGlue"/>.</param>
|
|
||||||
/// <param name="positionsToProxy">Parameter type positions (or null for the return type) for which types should also be proxied.</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>
|
|
||||||
private void ProxyMethod(InterfaceProxyFactory factory, TypeBuilder proxyBuilder, MethodInfo proxy, MethodInfo target, FieldBuilder instanceField, FieldBuilder glueField, ISet<int?> positionsToProxy, string sourceModID, string targetModID)
|
|
||||||
{
|
|
||||||
MethodBuilder methodBuilder = proxyBuilder.DefineMethod(proxy.Name, MethodAttributes.Public | MethodAttributes.Final | MethodAttributes.Virtual);
|
|
||||||
|
|
||||||
// set up generic arguments
|
|
||||||
Type[] proxyGenericArguments = proxy.GetGenericArguments();
|
|
||||||
string[] genericArgNames = proxyGenericArguments.Select(a => a.Name).ToArray();
|
|
||||||
GenericTypeParameterBuilder[] genericTypeParameterBuilders = proxyGenericArguments.Length == 0 ? null : methodBuilder.DefineGenericParameters(genericArgNames);
|
|
||||||
for (int i = 0; i < proxyGenericArguments.Length; i++)
|
|
||||||
genericTypeParameterBuilders[i].SetGenericParameterAttributes(proxyGenericArguments[i].GenericParameterAttributes);
|
|
||||||
|
|
||||||
// set up return type
|
|
||||||
Type returnType = proxy.ReturnType.IsGenericMethodParameter ? genericTypeParameterBuilders[proxy.ReturnType.GenericParameterPosition] : proxy.ReturnType;
|
|
||||||
methodBuilder.SetReturnType(returnType);
|
|
||||||
|
|
||||||
// set up parameters
|
|
||||||
var targetParameters = target.GetParameters();
|
|
||||||
Type[] argTypes = proxy.GetParameters()
|
|
||||||
.Select(a => a.ParameterType)
|
|
||||||
.Select(t => t.IsGenericMethodParameter ? genericTypeParameterBuilders[t.GenericParameterPosition] : t)
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
// proxy additional types
|
|
||||||
string returnValueProxyTypeName = null;
|
|
||||||
string[] parameterTargetToArgProxyTypeNames = new string[argTypes.Length];
|
|
||||||
string[] parameterArgToTargetUnproxyTypeNames = new string[argTypes.Length];
|
|
||||||
if (positionsToProxy.Count > 0)
|
|
||||||
{
|
|
||||||
foreach (int? position in positionsToProxy)
|
|
||||||
{
|
|
||||||
// we don't check for generics here, because earlier code does and generic positions won't end up here
|
|
||||||
if (position == null) // it's the return type
|
|
||||||
{
|
|
||||||
var builder = factory.ObtainBuilder(target.ReturnType, proxy.ReturnType, sourceModID, targetModID);
|
|
||||||
returnType = proxy.ReturnType;
|
|
||||||
returnValueProxyTypeName = builder.ProxyTypeName;
|
|
||||||
}
|
|
||||||
else // it's one of the parameters
|
|
||||||
{
|
|
||||||
bool isByRef = argTypes[position.Value].IsByRef;
|
|
||||||
var targetType = targetParameters[position.Value].ParameterType;
|
|
||||||
var argType = argTypes[position.Value];
|
|
||||||
|
|
||||||
var builder = factory.ObtainBuilder(targetType.GetNonRefType(), argType.GetNonRefType(), sourceModID, targetModID);
|
|
||||||
argTypes[position.Value] = argType;
|
|
||||||
parameterTargetToArgProxyTypeNames[position.Value] = builder.ProxyTypeName;
|
|
||||||
|
|
||||||
if (!targetParameters[position.Value].IsOut)
|
|
||||||
{
|
|
||||||
var argToTargetBuilder = factory.ObtainBuilder(argType.GetNonRefType(), targetType.GetNonRefType(), sourceModID, targetModID);
|
|
||||||
parameterArgToTargetUnproxyTypeNames[position.Value] = argToTargetBuilder.ProxyTypeName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
methodBuilder.SetReturnType(returnType);
|
|
||||||
}
|
|
||||||
|
|
||||||
methodBuilder.SetParameters(argTypes);
|
|
||||||
for (int i = 0; i < argTypes.Length; i++)
|
|
||||||
methodBuilder.DefineParameter(i, targetParameters[i].Attributes, targetParameters[i].Name);
|
|
||||||
|
|
||||||
// create method body
|
|
||||||
{
|
|
||||||
ILGenerator il = methodBuilder.GetILGenerator();
|
|
||||||
LocalBuilder[] inputLocals = new LocalBuilder[argTypes.Length];
|
|
||||||
LocalBuilder[] outputLocals = new LocalBuilder[argTypes.Length];
|
|
||||||
|
|
||||||
void ProxyIfNeededAndStore(LocalBuilder inputLocal, LocalBuilder outputLocal, string proxyTypeName, string unproxyTypeName)
|
|
||||||
{
|
|
||||||
if (proxyTypeName == null)
|
|
||||||
{
|
|
||||||
il.Emit(OpCodes.Ldloc, inputLocal);
|
|
||||||
il.Emit(OpCodes.Stloc, outputLocal);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var isNullLabel = il.DefineLabel();
|
|
||||||
il.Emit(OpCodes.Ldloc, inputLocal);
|
|
||||||
il.Emit(OpCodes.Brfalse, isNullLabel);
|
|
||||||
|
|
||||||
il.Emit(OpCodes.Ldarg_0);
|
|
||||||
il.Emit(OpCodes.Ldfld, glueField);
|
|
||||||
if (unproxyTypeName == null)
|
|
||||||
{
|
|
||||||
il.Emit(OpCodes.Ldstr, proxyTypeName);
|
|
||||||
il.Emit(OpCodes.Ldloc, inputLocal);
|
|
||||||
il.Emit(OpCodes.Call, ObtainInstanceForProxyTypeNameMethod);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
il.Emit(OpCodes.Ldstr, proxyTypeName);
|
|
||||||
il.Emit(OpCodes.Ldstr, unproxyTypeName);
|
|
||||||
il.Emit(OpCodes.Ldloc, inputLocal);
|
|
||||||
il.Emit(OpCodes.Call, UnproxyOrObtainInstanceForProxyTypeNameMethod);
|
|
||||||
}
|
|
||||||
il.Emit(OpCodes.Castclass, outputLocal.LocalType);
|
|
||||||
il.Emit(OpCodes.Stloc, outputLocal);
|
|
||||||
|
|
||||||
il.MarkLabel(isNullLabel);
|
|
||||||
}
|
|
||||||
|
|
||||||
// calling the proxied method
|
|
||||||
LocalBuilder resultInputLocal = target.ReturnType == typeof(void) ? null : il.DeclareLocal(target.ReturnType);
|
|
||||||
LocalBuilder resultOutputLocal = returnType == typeof(void) ? null : il.DeclareLocal(returnType);
|
|
||||||
il.Emit(OpCodes.Ldarg_0);
|
|
||||||
il.Emit(OpCodes.Ldfld, instanceField);
|
|
||||||
for (int i = 0; i < argTypes.Length; i++)
|
|
||||||
{
|
|
||||||
if (targetParameters[i].IsOut && parameterTargetToArgProxyTypeNames[i] != null) // out parameter, proxy on the way back
|
|
||||||
{
|
|
||||||
inputLocals[i] = il.DeclareLocal(targetParameters[i].ParameterType.GetNonRefType());
|
|
||||||
outputLocals[i] = il.DeclareLocal(argTypes[i].GetNonRefType());
|
|
||||||
il.Emit(OpCodes.Ldloca, inputLocals[i]);
|
|
||||||
}
|
|
||||||
else if (parameterArgToTargetUnproxyTypeNames[i] != null) // normal parameter, proxy on the way in
|
|
||||||
{
|
|
||||||
inputLocals[i] = il.DeclareLocal(argTypes[i].GetNonRefType());
|
|
||||||
outputLocals[i] = il.DeclareLocal(targetParameters[i].ParameterType.GetNonRefType());
|
|
||||||
il.Emit(OpCodes.Ldarg, i + 1);
|
|
||||||
il.Emit(OpCodes.Stloc, inputLocals[i]);
|
|
||||||
ProxyIfNeededAndStore(inputLocals[i], outputLocals[i], parameterArgToTargetUnproxyTypeNames[i], parameterTargetToArgProxyTypeNames[i]);
|
|
||||||
il.Emit(OpCodes.Ldloc, outputLocals[i]);
|
|
||||||
}
|
|
||||||
else // normal parameter, no proxying
|
|
||||||
{
|
|
||||||
il.Emit(OpCodes.Ldarg, i + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
il.Emit(target.IsVirtual ? OpCodes.Callvirt : OpCodes.Call, target);
|
|
||||||
if (target.ReturnType != typeof(void))
|
|
||||||
il.Emit(OpCodes.Stloc, resultInputLocal);
|
|
||||||
|
|
||||||
// proxying `out` parameters
|
|
||||||
for (int i = 0; i < argTypes.Length; i++)
|
|
||||||
{
|
|
||||||
if (parameterTargetToArgProxyTypeNames[i] == null)
|
|
||||||
continue;
|
|
||||||
if (!targetParameters[i].IsOut)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
ProxyIfNeededAndStore(inputLocals[i], outputLocals[i], parameterTargetToArgProxyTypeNames[i], null);
|
|
||||||
il.Emit(OpCodes.Ldarg, i + 1);
|
|
||||||
il.Emit(OpCodes.Ldloc, outputLocals[i]);
|
|
||||||
il.Emit(OpCodes.Stind_Ref);
|
|
||||||
}
|
|
||||||
|
|
||||||
// proxying return value
|
|
||||||
if (target.ReturnType != typeof(void))
|
|
||||||
ProxyIfNeededAndStore(resultInputLocal, resultOutputLocal, returnValueProxyTypeName, null);
|
|
||||||
|
|
||||||
// return result
|
|
||||||
if (target.ReturnType != typeof(void))
|
|
||||||
il.Emit(OpCodes.Ldloc, resultOutputLocal);
|
|
||||||
il.Emit(OpCodes.Ret);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Try to get a target instance for a given proxy instance.</summary>
|
|
||||||
/// <param name="potentialProxyInstance">The proxy instance to look for.</param>
|
|
||||||
/// <param name="targetInstance">The reference to store the found target instance in.</param>
|
|
||||||
public bool TryUnproxy(object potentialProxyInstance, out object targetInstance)
|
|
||||||
{
|
|
||||||
foreach ((object cachedTargetInstance, object cachedProxyInstance) in this.ProxyCache)
|
|
||||||
{
|
|
||||||
if (object.ReferenceEquals(potentialProxyInstance, cachedProxyInstance))
|
|
||||||
{
|
|
||||||
targetInstance = cachedTargetInstance;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
targetInstance = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>The part of a method that is being matched.</summary>
|
|
||||||
private enum MethodTypeMatchingPart
|
|
||||||
{
|
|
||||||
ReturnType, Parameter
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>The result of matching a target and a proxy type.</summary>
|
|
||||||
private enum MatchingTypesResult
|
|
||||||
{
|
|
||||||
False, IfProxied, True
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static class TypeExtensions
|
|
||||||
{
|
|
||||||
internal static Type GetNonRefType(this Type type)
|
|
||||||
{
|
|
||||||
return type.IsByRef ? type.GetElementType() : type;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,84 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
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 InterfaceProxyFactory
|
|
||||||
{
|
|
||||||
/*********
|
|
||||||
** Fields
|
|
||||||
*********/
|
|
||||||
/// <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, InterfaceProxyBuilder> Builders = new Dictionary<string, InterfaceProxyBuilder>();
|
|
||||||
|
|
||||||
|
|
||||||
/*********
|
|
||||||
** Public methods
|
|
||||||
*********/
|
|
||||||
/// <summary>Construct an instance.</summary>
|
|
||||||
public InterfaceProxyFactory()
|
|
||||||
{
|
|
||||||
AssemblyBuilder assemblyBuilder = AssemblyBuilder.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.");
|
|
||||||
|
|
||||||
// create instance
|
|
||||||
InterfaceProxyBuilder builder = this.ObtainBuilder(instance.GetType(), typeof(TInterface), sourceModID, targetModID);
|
|
||||||
return (TInterface)builder.ObtainInstance(instance, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal InterfaceProxyBuilder ObtainBuilder(Type targetType, Type interfaceType, string sourceModID, string targetModID)
|
|
||||||
{
|
|
||||||
lock (this.Builders)
|
|
||||||
{
|
|
||||||
// validate
|
|
||||||
if (!interfaceType.IsInterface)
|
|
||||||
throw new InvalidOperationException("The proxy type must be an interface, not a class.");
|
|
||||||
|
|
||||||
// get proxy type
|
|
||||||
string proxyTypeName = $"StardewModdingAPI.Proxies.From<{sourceModID}_{interfaceType.FullName}>_To<{targetModID}_{targetType.FullName}>";
|
|
||||||
if (!this.Builders.TryGetValue(proxyTypeName, out InterfaceProxyBuilder builder))
|
|
||||||
{
|
|
||||||
builder = new InterfaceProxyBuilder(targetType, interfaceType, proxyTypeName);
|
|
||||||
this.Builders[proxyTypeName] = builder;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
builder.SetupProxyType(this, this.ModuleBuilder, sourceModID, targetModID);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
this.Builders.Remove(proxyTypeName);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return builder;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal InterfaceProxyBuilder GetBuilderByProxyTypeName(string proxyTypeName)
|
|
||||||
{
|
|
||||||
lock (this.Builders)
|
|
||||||
{
|
|
||||||
return this.Builders.TryGetValue(proxyTypeName, out InterfaceProxyBuilder builder) ? builder : null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,34 +0,0 @@
|
||||||
namespace StardewModdingAPI.Framework.Reflection
|
|
||||||
{
|
|
||||||
/// <summary>Provides an interface for proxied types to create other proxied types.</summary>
|
|
||||||
public sealed class InterfaceProxyGlue
|
|
||||||
{
|
|
||||||
private readonly InterfaceProxyFactory Factory;
|
|
||||||
|
|
||||||
internal InterfaceProxyGlue(InterfaceProxyFactory factory)
|
|
||||||
{
|
|
||||||
this.Factory = factory;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Get an existing or create a new proxied instance by its type name.</summary>
|
|
||||||
/// <param name="proxyTypeName">The full name of the proxy type.</param>
|
|
||||||
/// <param name="toProxy">The target instance to proxy.</param>
|
|
||||||
public object ObtainInstanceForProxyTypeName(string proxyTypeName, object toProxy)
|
|
||||||
{
|
|
||||||
var builder = this.Factory.GetBuilderByProxyTypeName(proxyTypeName);
|
|
||||||
return builder.ObtainInstance(toProxy, this.Factory);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Try to unproxy, or get an existing, or create a new proxied instance by its type name.</summary>
|
|
||||||
/// <param name="proxyTypeName">The full name of the proxy type.</param>
|
|
||||||
/// <param name="unproxyTypeName">The full name of the reverse proxy type.</param>
|
|
||||||
/// <param name="toProxy">The target instance to proxy.</param>
|
|
||||||
public object UnproxyOrObtainInstanceForProxyTypeName(string proxyTypeName, string unproxyTypeName, object toProxy)
|
|
||||||
{
|
|
||||||
var unproxyBuilder = this.Factory.GetBuilderByProxyTypeName(unproxyTypeName);
|
|
||||||
if (unproxyBuilder.TryUnproxy(toProxy, out object targetInstance))
|
|
||||||
return targetInstance;
|
|
||||||
return this.ObtainInstanceForProxyTypeName(proxyTypeName, toProxy);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -47,6 +47,8 @@ using xTile.Display;
|
||||||
using MiniMonoModHotfix = MonoMod.Utils.MiniMonoModHotfix;
|
using MiniMonoModHotfix = MonoMod.Utils.MiniMonoModHotfix;
|
||||||
using PathUtilities = StardewModdingAPI.Toolkit.Utilities.PathUtilities;
|
using PathUtilities = StardewModdingAPI.Toolkit.Utilities.PathUtilities;
|
||||||
using SObject = StardewValley.Object;
|
using SObject = StardewValley.Object;
|
||||||
|
using Nanoray.Pintail;
|
||||||
|
using System.Reflection.Emit;
|
||||||
|
|
||||||
namespace StardewModdingAPI.Framework
|
namespace StardewModdingAPI.Framework
|
||||||
{
|
{
|
||||||
|
@ -1477,12 +1479,16 @@ namespace StardewModdingAPI.Framework
|
||||||
{
|
{
|
||||||
// init
|
// init
|
||||||
HashSet<string> suppressUpdateChecks = new HashSet<string>(this.Settings.SuppressUpdateChecks, StringComparer.OrdinalIgnoreCase);
|
HashSet<string> suppressUpdateChecks = new HashSet<string>(this.Settings.SuppressUpdateChecks, StringComparer.OrdinalIgnoreCase);
|
||||||
InterfaceProxyFactory proxyFactory = new InterfaceProxyFactory();
|
AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName($"StardewModdingAPI.Proxies, Version={this.GetType().Assembly.GetName().Version}, Culture=neutral"), AssemblyBuilderAccess.Run);
|
||||||
|
ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("StardewModdingAPI.Proxies");
|
||||||
|
IProxyManager<string> proxyManager = new DefaultProxyManager<string>(moduleBuilder, new DefaultProxyManagerConfiguration<string>(
|
||||||
|
proxyObjectInterfaceMarking: ProxyObjectInterfaceMarking.Disabled
|
||||||
|
));
|
||||||
|
|
||||||
// load mods
|
// load mods
|
||||||
foreach (IModMetadata mod in mods)
|
foreach (IModMetadata mod in mods)
|
||||||
{
|
{
|
||||||
if (!this.TryLoadMod(mod, mods, modAssemblyLoader, proxyFactory, jsonHelper, contentCore, modDatabase, suppressUpdateChecks, out ModFailReason? failReason, out string errorPhrase, out string errorDetails))
|
if (!this.TryLoadMod(mod, mods, modAssemblyLoader, proxyManager, jsonHelper, contentCore, modDatabase, suppressUpdateChecks, out ModFailReason? failReason, out string errorPhrase, out string errorDetails))
|
||||||
{
|
{
|
||||||
failReason ??= ModFailReason.LoadFailed;
|
failReason ??= ModFailReason.LoadFailed;
|
||||||
mod.SetStatus(ModMetadataStatus.Failed, failReason.Value, errorPhrase, errorDetails);
|
mod.SetStatus(ModMetadataStatus.Failed, failReason.Value, errorPhrase, errorDetails);
|
||||||
|
@ -1585,7 +1591,7 @@ namespace StardewModdingAPI.Framework
|
||||||
/// <param name="mod">The mod to load.</param>
|
/// <param name="mod">The mod to load.</param>
|
||||||
/// <param name="mods">The mods being loaded.</param>
|
/// <param name="mods">The mods being loaded.</param>
|
||||||
/// <param name="assemblyLoader">Preprocesses and loads mod assemblies.</param>
|
/// <param name="assemblyLoader">Preprocesses and loads mod assemblies.</param>
|
||||||
/// <param name="proxyFactory">Generates proxy classes to access mod APIs through an arbitrary interface.</param>
|
/// <param name="proxyManager">Generates proxy classes to access mod APIs through an arbitrary interface.</param>
|
||||||
/// <param name="jsonHelper">The JSON helper with which to read mods' JSON files.</param>
|
/// <param name="jsonHelper">The JSON helper with which to read mods' JSON files.</param>
|
||||||
/// <param name="contentCore">The content manager to use for mod content.</param>
|
/// <param name="contentCore">The content manager to use for mod content.</param>
|
||||||
/// <param name="modDatabase">Handles access to SMAPI's internal mod metadata list.</param>
|
/// <param name="modDatabase">Handles access to SMAPI's internal mod metadata list.</param>
|
||||||
|
@ -1594,7 +1600,7 @@ namespace StardewModdingAPI.Framework
|
||||||
/// <param name="errorReasonPhrase">The user-facing reason phrase explaining why the mod couldn't be loaded (if applicable).</param>
|
/// <param name="errorReasonPhrase">The user-facing reason phrase explaining why the mod couldn't be loaded (if applicable).</param>
|
||||||
/// <param name="errorDetails">More detailed details about the error intended for developers (if any).</param>
|
/// <param name="errorDetails">More detailed details about the error intended for developers (if any).</param>
|
||||||
/// <returns>Returns whether the mod was successfully loaded.</returns>
|
/// <returns>Returns whether the mod was successfully loaded.</returns>
|
||||||
private bool TryLoadMod(IModMetadata mod, IModMetadata[] mods, AssemblyLoader assemblyLoader, InterfaceProxyFactory proxyFactory, JsonHelper jsonHelper, ContentCoordinator contentCore, ModDatabase modDatabase, HashSet<string> suppressUpdateChecks, out ModFailReason? failReason, out string errorReasonPhrase, out string errorDetails)
|
private bool TryLoadMod(IModMetadata mod, IModMetadata[] mods, AssemblyLoader assemblyLoader, IProxyManager<string> proxyManager, JsonHelper jsonHelper, ContentCoordinator contentCore, ModDatabase modDatabase, HashSet<string> suppressUpdateChecks, out ModFailReason? failReason, out string errorReasonPhrase, out string errorDetails)
|
||||||
{
|
{
|
||||||
errorDetails = null;
|
errorDetails = null;
|
||||||
|
|
||||||
|
@ -1737,7 +1743,7 @@ namespace StardewModdingAPI.Framework
|
||||||
IContentPackHelper contentPackHelper = new ContentPackHelper(manifest.UniqueID, new Lazy<IContentPack[]>(GetContentPacks), CreateFakeContentPack);
|
IContentPackHelper contentPackHelper = new ContentPackHelper(manifest.UniqueID, new Lazy<IContentPack[]>(GetContentPacks), CreateFakeContentPack);
|
||||||
IDataHelper dataHelper = new DataHelper(manifest.UniqueID, mod.DirectoryPath, jsonHelper);
|
IDataHelper dataHelper = new DataHelper(manifest.UniqueID, mod.DirectoryPath, jsonHelper);
|
||||||
IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, mod.DisplayName, this.Reflection);
|
IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, mod.DisplayName, this.Reflection);
|
||||||
IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry, proxyFactory, monitor);
|
IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry, proxyManager, monitor);
|
||||||
IMultiplayerHelper multiplayerHelper = new MultiplayerHelper(manifest.UniqueID, this.Multiplayer);
|
IMultiplayerHelper multiplayerHelper = new MultiplayerHelper(manifest.UniqueID, this.Multiplayer);
|
||||||
|
|
||||||
modHelper = new ModHelper(manifest.UniqueID, mod.DirectoryPath, () => this.GetCurrentGameInstance().Input, events, contentHelper, contentPackHelper, commandHelper, dataHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper);
|
modHelper = new ModHelper(manifest.UniqueID, mod.DirectoryPath, () => this.GetCurrentGameInstance().Input, events, contentHelper, contentPackHelper, commandHelper, dataHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper);
|
||||||
|
|
|
@ -25,12 +25,5 @@ namespace StardewModdingAPI
|
||||||
/// <typeparam name="TInterface">The interface which matches the properties and methods you intend to access.</typeparam>
|
/// <typeparam name="TInterface">The interface which matches the properties and methods you intend to access.</typeparam>
|
||||||
/// <param name="uniqueID">The mod's unique ID.</param>
|
/// <param name="uniqueID">The mod's unique ID.</param>
|
||||||
TInterface GetApi<TInterface>(string uniqueID) where TInterface : class;
|
TInterface GetApi<TInterface>(string uniqueID) where TInterface : class;
|
||||||
|
|
||||||
/// <summary>Try to proxy (or unproxy back) the given object to a given interface provided by a mod.</summary>
|
|
||||||
/// <typeparam name="TInterface">The interface type to proxy (or unproxy) to.</typeparam>
|
|
||||||
/// <param name="uniqueID">The mod's unique ID.</param>
|
|
||||||
/// <param name="toProxy">The object to try to proxy (or unproxy back).</param>
|
|
||||||
/// <param name="proxy">The reference to store the proxied (or unproxied) object back.</param>
|
|
||||||
bool TryProxy<TInterface>(string uniqueID, object toProxy, out TInterface proxy) where TInterface : class;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,7 @@
|
||||||
<PackageReference Include="Platonymous.TMXTile" Version="1.5.9" />
|
<PackageReference Include="Platonymous.TMXTile" Version="1.5.9" />
|
||||||
<PackageReference Include="System.Reflection.Emit" Version="4.7.0" />
|
<PackageReference Include="System.Reflection.Emit" Version="4.7.0" />
|
||||||
<PackageReference Include="System.Runtime.Caching" Version="5.0.0" />
|
<PackageReference Include="System.Runtime.Caching" Version="5.0.0" />
|
||||||
|
<PackageReference Include="Pintail" Version="1.2.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
Loading…
Reference in New Issue