allow switching between Pintail & original API proxying

This commit is contained in:
Jesse Plamondon-Willard 2022-04-16 19:15:50 -04:00
parent 7dec519234
commit 0a69cb4bf7
No known key found for this signature in database
GPG Key ID: CF8B1456B3E29F49
10 changed files with 259 additions and 39 deletions

View File

@ -29,6 +29,12 @@ namespace SMAPI.Tests.Core
/// <summary>The random number generator with which to create sample values.</summary> /// <summary>The random number generator with which to create sample values.</summary>
private readonly Random Random = new(); private readonly Random Random = new();
/// <summary>Sample user inputs for season names.</summary>
private static readonly IInterfaceProxyFactory[] ProxyFactories = {
new InterfaceProxyFactory(),
new OriginalInterfaceProxyFactory()
};
/********* /*********
** Unit tests ** Unit tests
@ -37,8 +43,9 @@ namespace SMAPI.Tests.Core
** Events ** Events
****/ ****/
/// <summary>Assert that an event field can be proxied correctly.</summary> /// <summary>Assert that an event field can be proxied correctly.</summary>
/// <param name="proxyFactory">The proxy factory to test.</param>
[Test] [Test]
public void CanProxy_EventField() public void CanProxy_EventField([ValueSource(nameof(InterfaceProxyTests.ProxyFactories))] IInterfaceProxyFactory proxyFactory)
{ {
// arrange // arrange
ProviderMod providerMod = new(); ProviderMod providerMod = new();
@ -46,7 +53,7 @@ namespace SMAPI.Tests.Core
int expectedValue = this.Random.Next(); int expectedValue = this.Random.Next();
// act // act
ISimpleApi proxy = this.GetProxy(implementation); ISimpleApi proxy = this.GetProxy(proxyFactory, implementation);
new ApiConsumer().UseEventField(proxy, out Func<(int timesCalled, int lastValue)> getValues); new ApiConsumer().UseEventField(proxy, out Func<(int timesCalled, int lastValue)> getValues);
providerMod.RaiseEvent(expectedValue); providerMod.RaiseEvent(expectedValue);
(int timesCalled, int lastValue) = getValues(); (int timesCalled, int lastValue) = getValues();
@ -57,8 +64,9 @@ namespace SMAPI.Tests.Core
} }
/// <summary>Assert that an event property can be proxied correctly.</summary> /// <summary>Assert that an event property can be proxied correctly.</summary>
/// <param name="proxyFactory">The proxy factory to test.</param>
[Test] [Test]
public void CanProxy_EventProperty() public void CanProxy_EventProperty([ValueSource(nameof(InterfaceProxyTests.ProxyFactories))] IInterfaceProxyFactory proxyFactory)
{ {
// arrange // arrange
ProviderMod providerMod = new(); ProviderMod providerMod = new();
@ -66,7 +74,7 @@ namespace SMAPI.Tests.Core
int expectedValue = this.Random.Next(); int expectedValue = this.Random.Next();
// act // act
ISimpleApi proxy = this.GetProxy(implementation); ISimpleApi proxy = this.GetProxy(proxyFactory, implementation);
new ApiConsumer().UseEventProperty(proxy, out Func<(int timesCalled, int lastValue)> getValues); new ApiConsumer().UseEventProperty(proxy, out Func<(int timesCalled, int lastValue)> getValues);
providerMod.RaiseEvent(expectedValue); providerMod.RaiseEvent(expectedValue);
(int timesCalled, int lastValue) = getValues(); (int timesCalled, int lastValue) = getValues();
@ -80,10 +88,10 @@ namespace SMAPI.Tests.Core
** Properties ** Properties
****/ ****/
/// <summary>Assert that properties can be proxied correctly.</summary> /// <summary>Assert that properties can be proxied correctly.</summary>
/// <param name="proxyFactory">The proxy factory to test.</param>
/// <param name="setVia">Whether to set the properties through the <c>provider mod</c> or <c>proxy interface</c>.</param> /// <param name="setVia">Whether to set the properties through the <c>provider mod</c> or <c>proxy interface</c>.</param>
[TestCase("set via provider mod")] [Test]
[TestCase("set via proxy interface")] public void CanProxy_Properties([ValueSource(nameof(InterfaceProxyTests.ProxyFactories))] IInterfaceProxyFactory proxyFactory, [Values("set via provider mod", "set via proxy interface")] string setVia)
public void CanProxy_Properties(string setVia)
{ {
// arrange // arrange
ProviderMod providerMod = new(); ProviderMod providerMod = new();
@ -98,7 +106,7 @@ namespace SMAPI.Tests.Core
BindingFlags expectedEnum = BindingFlags.Instance | BindingFlags.Public; BindingFlags expectedEnum = BindingFlags.Instance | BindingFlags.Public;
// act // act
ISimpleApi proxy = this.GetProxy(implementation); ISimpleApi proxy = this.GetProxy(proxyFactory, implementation);
switch (setVia) switch (setVia)
{ {
case "set via provider mod": case "set via provider mod":
@ -198,27 +206,29 @@ namespace SMAPI.Tests.Core
} }
/// <summary>Assert that a simple method with no return value can be proxied correctly.</summary> /// <summary>Assert that a simple method with no return value can be proxied correctly.</summary>
/// <param name="proxyFactory">The proxy factory to test.</param>
[Test] [Test]
public void CanProxy_SimpleMethod_Void() public void CanProxy_SimpleMethod_Void([ValueSource(nameof(InterfaceProxyTests.ProxyFactories))] IInterfaceProxyFactory proxyFactory)
{ {
// arrange // arrange
object implementation = new ProviderMod().GetModApi(); object implementation = new ProviderMod().GetModApi();
// act // act
ISimpleApi proxy = this.GetProxy(implementation); ISimpleApi proxy = this.GetProxy(proxyFactory, implementation);
proxy.GetNothing(); proxy.GetNothing();
} }
/// <summary>Assert that a simple int method can be proxied correctly.</summary> /// <summary>Assert that a simple int method can be proxied correctly.</summary>
/// <param name="proxyFactory">The proxy factory to test.</param>
[Test] [Test]
public void CanProxy_SimpleMethod_Int() public void CanProxy_SimpleMethod_Int([ValueSource(nameof(InterfaceProxyTests.ProxyFactories))] IInterfaceProxyFactory proxyFactory)
{ {
// arrange // arrange
object implementation = new ProviderMod().GetModApi(); object implementation = new ProviderMod().GetModApi();
int expectedValue = this.Random.Next(); int expectedValue = this.Random.Next();
// act // act
ISimpleApi proxy = this.GetProxy(implementation); ISimpleApi proxy = this.GetProxy(proxyFactory, implementation);
int actualValue = proxy.GetInt(expectedValue); int actualValue = proxy.GetInt(expectedValue);
// assert // assert
@ -226,15 +236,16 @@ namespace SMAPI.Tests.Core
} }
/// <summary>Assert that a simple object method can be proxied correctly.</summary> /// <summary>Assert that a simple object method can be proxied correctly.</summary>
/// <param name="proxyFactory">The proxy factory to test.</param>
[Test] [Test]
public void CanProxy_SimpleMethod_Object() public void CanProxy_SimpleMethod_Object([ValueSource(nameof(InterfaceProxyTests.ProxyFactories))] IInterfaceProxyFactory proxyFactory)
{ {
// arrange // arrange
object implementation = new ProviderMod().GetModApi(); object implementation = new ProviderMod().GetModApi();
object expectedValue = new(); object expectedValue = new();
// act // act
ISimpleApi proxy = this.GetProxy(implementation); ISimpleApi proxy = this.GetProxy(proxyFactory, implementation);
object actualValue = proxy.GetObject(expectedValue); object actualValue = proxy.GetObject(expectedValue);
// assert // assert
@ -242,15 +253,16 @@ namespace SMAPI.Tests.Core
} }
/// <summary>Assert that a simple list method can be proxied correctly.</summary> /// <summary>Assert that a simple list method can be proxied correctly.</summary>
/// <param name="proxyFactory">The proxy factory to test.</param>
[Test] [Test]
public void CanProxy_SimpleMethod_List() public void CanProxy_SimpleMethod_List([ValueSource(nameof(InterfaceProxyTests.ProxyFactories))] IInterfaceProxyFactory proxyFactory)
{ {
// arrange // arrange
object implementation = new ProviderMod().GetModApi(); object implementation = new ProviderMod().GetModApi();
string expectedValue = this.GetRandomString(); string expectedValue = this.GetRandomString();
// act // act
ISimpleApi proxy = this.GetProxy(implementation); ISimpleApi proxy = this.GetProxy(proxyFactory, implementation);
IList<string> actualValue = proxy.GetList(expectedValue); IList<string> actualValue = proxy.GetList(expectedValue);
// assert // assert
@ -258,15 +270,16 @@ namespace SMAPI.Tests.Core
} }
/// <summary>Assert that a simple list with interface method can be proxied correctly.</summary> /// <summary>Assert that a simple list with interface method can be proxied correctly.</summary>
/// <param name="proxyFactory">The proxy factory to test.</param>
[Test] [Test]
public void CanProxy_SimpleMethod_ListWithInterface() public void CanProxy_SimpleMethod_ListWithInterface([ValueSource(nameof(InterfaceProxyTests.ProxyFactories))] IInterfaceProxyFactory proxyFactory)
{ {
// arrange // arrange
object implementation = new ProviderMod().GetModApi(); object implementation = new ProviderMod().GetModApi();
string expectedValue = this.GetRandomString(); string expectedValue = this.GetRandomString();
// act // act
ISimpleApi proxy = this.GetProxy(implementation); ISimpleApi proxy = this.GetProxy(proxyFactory, implementation);
IList<string> actualValue = proxy.GetListWithInterface(expectedValue); IList<string> actualValue = proxy.GetListWithInterface(expectedValue);
// assert // assert
@ -274,8 +287,9 @@ namespace SMAPI.Tests.Core
} }
/// <summary>Assert that a simple method which returns generic types can be proxied correctly.</summary> /// <summary>Assert that a simple method which returns generic types can be proxied correctly.</summary>
/// <param name="proxyFactory">The proxy factory to test.</param>
[Test] [Test]
public void CanProxy_SimpleMethod_GenericTypes() public void CanProxy_SimpleMethod_GenericTypes([ValueSource(nameof(InterfaceProxyTests.ProxyFactories))] IInterfaceProxyFactory proxyFactory)
{ {
// arrange // arrange
object implementation = new ProviderMod().GetModApi(); object implementation = new ProviderMod().GetModApi();
@ -283,7 +297,7 @@ namespace SMAPI.Tests.Core
string expectedValue = this.GetRandomString(); string expectedValue = this.GetRandomString();
// act // act
ISimpleApi proxy = this.GetProxy(implementation); ISimpleApi proxy = this.GetProxy(proxyFactory, implementation);
IDictionary<string, IList<string>> actualValue = proxy.GetGenerics(expectedKey, expectedValue); IDictionary<string, IList<string>> actualValue = proxy.GetGenerics(expectedKey, expectedValue);
// assert // assert
@ -294,16 +308,17 @@ namespace SMAPI.Tests.Core
} }
/// <summary>Assert that a simple lambda method can be proxied correctly.</summary> /// <summary>Assert that a simple lambda method can be proxied correctly.</summary>
/// <param name="proxyFactory">The proxy factory to test.</param>
[Test] [Test]
[SuppressMessage("ReSharper", "ConvertToLocalFunction")] [SuppressMessage("ReSharper", "ConvertToLocalFunction")]
public void CanProxy_SimpleMethod_Lambda() public void CanProxy_SimpleMethod_Lambda([ValueSource(nameof(InterfaceProxyTests.ProxyFactories))] IInterfaceProxyFactory proxyFactory)
{ {
// arrange // arrange
object implementation = new ProviderMod().GetModApi(); object implementation = new ProviderMod().GetModApi();
Func<string, string> expectedValue = _ => "test"; Func<string, string> expectedValue = _ => "test";
// act // act
ISimpleApi proxy = this.GetProxy(implementation); ISimpleApi proxy = this.GetProxy(proxyFactory, implementation);
object actualValue = proxy.GetObject(expectedValue); object actualValue = proxy.GetObject(expectedValue);
// assert // assert
@ -311,16 +326,17 @@ namespace SMAPI.Tests.Core
} }
/// <summary>Assert that a method with out parameters can be proxied correctly.</summary> /// <summary>Assert that a method with out parameters can be proxied correctly.</summary>
/// <param name="proxyFactory">The proxy factory to test.</param>
[Test] [Test]
[SuppressMessage("ReSharper", "ConvertToLocalFunction")] [SuppressMessage("ReSharper", "ConvertToLocalFunction")]
public void CanProxy_Method_OutParameters() public void CanProxy_Method_OutParameters([ValueSource(nameof(InterfaceProxyTests.ProxyFactories))] IInterfaceProxyFactory proxyFactory)
{ {
// arrange // arrange
object implementation = new ProviderMod().GetModApi(); object implementation = new ProviderMod().GetModApi();
const int expectedNumber = 42; const int expectedNumber = 42;
// act // act
ISimpleApi proxy = this.GetProxy(implementation); ISimpleApi proxy = this.GetProxy(proxyFactory, implementation);
bool result = proxy.TryGetOutParameter( bool result = proxy.TryGetOutParameter(
inputNumber: expectedNumber, inputNumber: expectedNumber,
@ -374,10 +390,10 @@ namespace SMAPI.Tests.Core
} }
/// <summary>Get a proxy API instance.</summary> /// <summary>Get a proxy API instance.</summary>
/// <param name="proxyFactory">The proxy factory to use.</param>
/// <param name="implementation">The underlying API instance.</param> /// <param name="implementation">The underlying API instance.</param>
private ISimpleApi GetProxy(object implementation) private ISimpleApi GetProxy(IInterfaceProxyFactory proxyFactory, object implementation)
{ {
var proxyFactory = new InterfaceProxyFactory();
return proxyFactory.CreateProxy<ISimpleApi>(implementation, this.FromModId, this.ToModId); return proxyFactory.CreateProxy<ISimpleApi>(implementation, this.FromModId, this.ToModId);
} }
} }

View File

@ -61,11 +61,13 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=Pastebin/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Pastebin/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=pathfinding/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=pathfinding/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Pathoschild/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Pathoschild/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Pintail/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=premultiplied/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=premultiplied/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=premultiply/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=premultiply/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Prenormalize/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Prenormalize/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Preprocesses/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Preprocesses/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=prerelease/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=prerelease/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=proxying/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=pufferchick/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=pufferchick/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=rasterizer/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=rasterizer/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=reimplements/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=reimplements/@EntryIndexedValue">True</s:Boolean>

View File

@ -19,7 +19,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
private readonly HashSet<string> AccessedModApis = new(); private readonly HashSet<string> AccessedModApis = new();
/// <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 IInterfaceProxyFactory ProxyFactory;
/********* /*********
@ -30,7 +30,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <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="proxyFactory">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(IModMetadata mod, ModRegistry registry, InterfaceProxyFactory proxyFactory, IMonitor monitor) public ModRegistryHelper(IModMetadata mod, ModRegistry registry, IInterfaceProxyFactory proxyFactory, IMonitor monitor)
: base(mod) : base(mod)
{ {
this.Registry = registry; this.Registry = registry;

View File

@ -22,7 +22,8 @@ namespace StardewModdingAPI.Framework.Models
[nameof(VerboseLogging)] = false, [nameof(VerboseLogging)] = false,
[nameof(LogNetworkTraffic)] = false, [nameof(LogNetworkTraffic)] = false,
[nameof(RewriteMods)] = true, [nameof(RewriteMods)] = true,
[nameof(AggressiveMemoryOptimizations)] = false [nameof(AggressiveMemoryOptimizations)] = false,
[nameof(UsePintail)] = true
}; };
/// <summary>The default values for <see cref="SuppressUpdateChecks"/>, to log changes if different.</summary> /// <summary>The default values for <see cref="SuppressUpdateChecks"/>, to log changes if different.</summary>
@ -64,6 +65,9 @@ namespace StardewModdingAPI.Framework.Models
/// <summary>Whether to enable more aggressive memory optimizations.</summary> /// <summary>Whether to enable more aggressive memory optimizations.</summary>
public bool AggressiveMemoryOptimizations { get; } public bool AggressiveMemoryOptimizations { get; }
/// <summary>Whether to use the experimental Pintail API proxying library, instead of the original proxying built into SMAPI itself.</summary>
public bool UsePintail { get; }
/// <summary>Whether SMAPI should log network traffic. Best combined with <see cref="VerboseLogging"/>, which includes network metadata.</summary> /// <summary>Whether SMAPI should log network traffic. Best combined with <see cref="VerboseLogging"/>, which includes network metadata.</summary>
public bool LogNetworkTraffic { get; } public bool LogNetworkTraffic { get; }
@ -87,10 +91,11 @@ namespace StardewModdingAPI.Framework.Models
/// <param name="verboseLogging">Whether SMAPI should log more information about the game context.</param> /// <param name="verboseLogging">Whether SMAPI should log more information about the game context.</param>
/// <param name="rewriteMods">Whether SMAPI should rewrite mods for compatibility.</param> /// <param name="rewriteMods">Whether SMAPI should rewrite mods for compatibility.</param>
/// <param name="aggressiveMemoryOptimizations">Whether to enable more aggressive memory optimizations.</param> /// <param name="aggressiveMemoryOptimizations">Whether to enable more aggressive memory optimizations.</param>
/// <param name="usePintail">Whether to use the experimental Pintail API proxying library, instead of the original proxying built into SMAPI itself.</param>
/// <param name="logNetworkTraffic">Whether SMAPI should log network traffic.</param> /// <param name="logNetworkTraffic">Whether SMAPI should log network traffic.</param>
/// <param name="consoleColors">The colors to use for text written to the SMAPI console.</param> /// <param name="consoleColors">The colors to use for text written to the SMAPI console.</param>
/// <param name="suppressUpdateChecks">The mod IDs SMAPI should ignore when performing update checks or validating update keys.</param> /// <param name="suppressUpdateChecks">The mod IDs SMAPI should ignore when performing update checks or validating update keys.</param>
public SConfig(bool developerMode, bool checkForUpdates, bool? paranoidWarnings, bool? useBetaChannel, string gitHubProjectName, string webApiBaseUrl, bool verboseLogging, bool? rewriteMods, bool? aggressiveMemoryOptimizations, bool logNetworkTraffic, ColorSchemeConfig consoleColors, string[]? suppressUpdateChecks) public SConfig(bool developerMode, bool checkForUpdates, bool? paranoidWarnings, bool? useBetaChannel, string gitHubProjectName, string webApiBaseUrl, bool verboseLogging, bool? rewriteMods, bool? aggressiveMemoryOptimizations, bool usePintail, bool logNetworkTraffic, ColorSchemeConfig consoleColors, string[]? suppressUpdateChecks)
{ {
this.DeveloperMode = developerMode; this.DeveloperMode = developerMode;
this.CheckForUpdates = checkForUpdates; this.CheckForUpdates = checkForUpdates;
@ -101,6 +106,7 @@ namespace StardewModdingAPI.Framework.Models
this.VerboseLogging = verboseLogging; this.VerboseLogging = verboseLogging;
this.RewriteMods = rewriteMods ?? (bool)SConfig.DefaultValues[nameof(SConfig.RewriteMods)]; this.RewriteMods = rewriteMods ?? (bool)SConfig.DefaultValues[nameof(SConfig.RewriteMods)];
this.AggressiveMemoryOptimizations = aggressiveMemoryOptimizations ?? (bool)SConfig.DefaultValues[nameof(SConfig.AggressiveMemoryOptimizations)]; this.AggressiveMemoryOptimizations = aggressiveMemoryOptimizations ?? (bool)SConfig.DefaultValues[nameof(SConfig.AggressiveMemoryOptimizations)];
this.UsePintail = usePintail;
this.LogNetworkTraffic = logNetworkTraffic; this.LogNetworkTraffic = logNetworkTraffic;
this.ConsoleColors = consoleColors; this.ConsoleColors = consoleColors;
this.SuppressUpdateChecks = suppressUpdateChecks ?? Array.Empty<string>(); this.SuppressUpdateChecks = suppressUpdateChecks ?? Array.Empty<string>();

View File

@ -0,0 +1,17 @@
namespace StardewModdingAPI.Framework.Reflection
{
/// <summary>Generates proxy classes to access mod APIs through an arbitrary interface.</summary>
internal interface IInterfaceProxyFactory
{
/*********
** Methods
*********/
/// <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>
TInterface CreateProxy<TInterface>(object instance, string sourceModID, string targetModID)
where TInterface : class;
}
}

View File

@ -4,8 +4,8 @@ using Nanoray.Pintail;
namespace StardewModdingAPI.Framework.Reflection namespace StardewModdingAPI.Framework.Reflection
{ {
/// <summary>Generates proxy classes to access mod APIs through an arbitrary interface.</summary> /// <inheritdoc />
internal class InterfaceProxyFactory internal class InterfaceProxyFactory : IInterfaceProxyFactory
{ {
/********* /*********
** Fields ** Fields
@ -28,11 +28,7 @@ namespace StardewModdingAPI.Framework.Reflection
)); ));
} }
/// <summary>Create an API proxy.</summary> /// <inheritdoc />
/// <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) public TInterface CreateProxy<TInterface>(object instance, string sourceModID, string targetModID)
where TInterface : class where TInterface : class
{ {

View File

@ -0,0 +1,118 @@
using System;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
namespace StardewModdingAPI.Framework.Reflection
{
/// <summary>Generates a proxy class to access a mod API through an arbitrary interface.</summary>
internal class OriginalInterfaceProxyBuilder
{
/*********
** Fields
*********/
/// <summary>The target class type.</summary>
private readonly Type TargetType;
/// <summary>The generated proxy type.</summary>
private readonly Type ProxyType;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="name">The type name to generate.</param>
/// <param name="moduleBuilder">The CLR module in which to create proxy classes.</param>
/// <param name="interfaceType">The interface type to implement.</param>
/// <param name="targetType">The target type.</param>
public OriginalInterfaceProxyBuilder(string name, ModuleBuilder moduleBuilder, Type interfaceType, Type targetType)
{
// validate
if (name == null)
throw new ArgumentNullException(nameof(name));
if (targetType == null)
throw new ArgumentNullException(nameof(targetType));
// define proxy type
TypeBuilder proxyBuilder = moduleBuilder.DefineType(name, TypeAttributes.Public | TypeAttributes.Class);
proxyBuilder.AddInterfaceImplementation(interfaceType);
// create field to store target instance
FieldBuilder targetField = proxyBuilder.DefineField("__Target", targetType, FieldAttributes.Private);
// create constructor which accepts target instance and sets field
{
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(Type.EmptyTypes)!); // 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.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, targetField);
}
// save info
this.TargetType = targetType;
this.ProxyType = proxyBuilder.CreateType()!;
}
/// <summary>Create an instance of the proxy for a target instance.</summary>
/// <param name="targetInstance">The target instance.</param>
public object CreateInstance(object targetInstance)
{
ConstructorInfo? constructor = this.ProxyType.GetConstructor(new[] { this.TargetType });
if (constructor == null)
throw new InvalidOperationException($"Couldn't find the constructor for generated proxy type '{this.ProxyType.Name}'."); // should never happen
return constructor.Invoke(new[] { targetInstance });
}
/*********
** Private methods
*********/
/// <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);
}
}
}
}

View File

@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
namespace StardewModdingAPI.Framework.Reflection
{
/// <inheritdoc />
internal class OriginalInterfaceProxyFactory : IInterfaceProxyFactory
{
/*********
** 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, OriginalInterfaceProxyBuilder> Builders = new Dictionary<string, OriginalInterfaceProxyBuilder>();
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
public OriginalInterfaceProxyFactory()
{
AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName($"StardewModdingAPI.Proxies, Version={this.GetType().Assembly.GetName().Version}, Culture=neutral"), AssemblyBuilderAccess.Run);
this.ModuleBuilder = assemblyBuilder.DefineDynamicModule("StardewModdingAPI.Proxies");
}
/// <inheritdoc />
public TInterface CreateProxy<TInterface>(object instance, string sourceModID, string targetModID)
where TInterface : class
{
lock (this.Builders)
{
// 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.Builders.TryGetValue(proxyTypeName, out OriginalInterfaceProxyBuilder? builder))
{
builder = new OriginalInterfaceProxyBuilder(proxyTypeName, this.ModuleBuilder, typeof(TInterface), targetType);
this.Builders[proxyTypeName] = builder;
}
// create instance
return (TInterface)builder.CreateInstance(instance);
}
}
}
}

View File

@ -1560,7 +1560,9 @@ 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(); IInterfaceProxyFactory proxyFactory = this.Settings.UsePintail
? new InterfaceProxyFactory()
: new OriginalInterfaceProxyFactory();
// load mods // load mods
foreach (IModMetadata mod in mods) foreach (IModMetadata mod in mods)
@ -1699,7 +1701,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, [NotNullWhen(false)] out ModFailReason? failReason, out string? errorReasonPhrase, out string? errorDetails) private bool TryLoadMod(IModMetadata mod, IModMetadata[] mods, AssemblyLoader assemblyLoader, IInterfaceProxyFactory proxyFactory, JsonHelper jsonHelper, ContentCoordinator contentCore, ModDatabase modDatabase, HashSet<string> suppressUpdateChecks, [NotNullWhen(false)] out ModFailReason? failReason, out string? errorReasonPhrase, out string? errorDetails)
{ {
errorDetails = null; errorDetails = null;

View File

@ -46,6 +46,12 @@ copy all the settings, or you may cause bugs due to overridden changes in future
*/ */
"AggressiveMemoryOptimizations": false, "AggressiveMemoryOptimizations": false,
/**
* Whether to use the experimental Pintail API proxying library, instead of the original
* proxying built into SMAPI itself.
*/
"UsePintail": true,
/** /**
* Whether to add a section to the 'mod issues' list for mods which directly use potentially * Whether to add a section to the 'mod issues' list for mods which directly use potentially
* sensitive .NET APIs like file or shell access. Note that many mods do this legitimately as * sensitive .NET APIs like file or shell access. Note that many mods do this legitimately as