summaryrefslogtreecommitdiff
path: root/src/SMAPI/Framework
diff options
context:
space:
mode:
authorJesse Plamondon-Willard <Pathoschild@users.noreply.github.com>2022-04-16 19:15:50 -0400
committerJesse Plamondon-Willard <Pathoschild@users.noreply.github.com>2022-04-16 19:15:50 -0400
commit0a69cb4bf71e6e822e595141ce5f24009e509246 (patch)
treeccdad278a6442b2aa31cee17501d4e89a4898653 /src/SMAPI/Framework
parent7dec51923418b269e111a266edb319ff3b0cb118 (diff)
downloadSMAPI-0a69cb4bf71e6e822e595141ce5f24009e509246.tar.gz
SMAPI-0a69cb4bf71e6e822e595141ce5f24009e509246.tar.bz2
SMAPI-0a69cb4bf71e6e822e595141ce5f24009e509246.zip
allow switching between Pintail & original API proxying
Diffstat (limited to 'src/SMAPI/Framework')
-rw-r--r--src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs4
-rw-r--r--src/SMAPI/Framework/Models/SConfig.cs10
-rw-r--r--src/SMAPI/Framework/Reflection/IInterfaceProxyFactory.cs17
-rw-r--r--src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs10
-rw-r--r--src/SMAPI/Framework/Reflection/OriginalInterfaceProxyBuilder.cs118
-rw-r--r--src/SMAPI/Framework/Reflection/OriginalInterfaceProxyFactory.cs57
-rw-r--r--src/SMAPI/Framework/SCore.cs6
7 files changed, 209 insertions, 13 deletions
diff --git a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs
index 39cef758..348ba225 100644
--- a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs
@@ -19,7 +19,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
private readonly HashSet<string> AccessedModApis = new();
/// <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="proxyFactory">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(IModMetadata mod, ModRegistry registry, InterfaceProxyFactory proxyFactory, IMonitor monitor)
+ public ModRegistryHelper(IModMetadata mod, ModRegistry registry, IInterfaceProxyFactory proxyFactory, IMonitor monitor)
: base(mod)
{
this.Registry = registry;
diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs
index d626ab4d..1a43c1fc 100644
--- a/src/SMAPI/Framework/Models/SConfig.cs
+++ b/src/SMAPI/Framework/Models/SConfig.cs
@@ -22,7 +22,8 @@ namespace StardewModdingAPI.Framework.Models
[nameof(VerboseLogging)] = false,
[nameof(LogNetworkTraffic)] = false,
[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>
@@ -64,6 +65,9 @@ namespace StardewModdingAPI.Framework.Models
/// <summary>Whether to enable more aggressive memory optimizations.</summary>
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>
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="rewriteMods">Whether SMAPI should rewrite mods for compatibility.</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="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>
- 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.CheckForUpdates = checkForUpdates;
@@ -101,6 +106,7 @@ namespace StardewModdingAPI.Framework.Models
this.VerboseLogging = verboseLogging;
this.RewriteMods = rewriteMods ?? (bool)SConfig.DefaultValues[nameof(SConfig.RewriteMods)];
this.AggressiveMemoryOptimizations = aggressiveMemoryOptimizations ?? (bool)SConfig.DefaultValues[nameof(SConfig.AggressiveMemoryOptimizations)];
+ this.UsePintail = usePintail;
this.LogNetworkTraffic = logNetworkTraffic;
this.ConsoleColors = consoleColors;
this.SuppressUpdateChecks = suppressUpdateChecks ?? Array.Empty<string>();
diff --git a/src/SMAPI/Framework/Reflection/IInterfaceProxyFactory.cs b/src/SMAPI/Framework/Reflection/IInterfaceProxyFactory.cs
new file mode 100644
index 00000000..6429db58
--- /dev/null
+++ b/src/SMAPI/Framework/Reflection/IInterfaceProxyFactory.cs
@@ -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;
+ }
+}
diff --git a/src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs b/src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs
index 40adde8e..694c563d 100644
--- a/src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs
+++ b/src/SMAPI/Framework/Reflection/InterfaceProxyFactory.cs
@@ -4,8 +4,8 @@ using Nanoray.Pintail;
namespace StardewModdingAPI.Framework.Reflection
{
- /// <summary>Generates proxy classes to access mod APIs through an arbitrary interface.</summary>
- internal class InterfaceProxyFactory
+ /// <inheritdoc />
+ internal class InterfaceProxyFactory : IInterfaceProxyFactory
{
/*********
** Fields
@@ -28,11 +28,7 @@ namespace StardewModdingAPI.Framework.Reflection
));
}
- /// <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>
+ /// <inheritdoc />
public TInterface CreateProxy<TInterface>(object instance, string sourceModID, string targetModID)
where TInterface : class
{
diff --git a/src/SMAPI/Framework/Reflection/OriginalInterfaceProxyBuilder.cs b/src/SMAPI/Framework/Reflection/OriginalInterfaceProxyBuilder.cs
new file mode 100644
index 00000000..9576f768
--- /dev/null
+++ b/src/SMAPI/Framework/Reflection/OriginalInterfaceProxyBuilder.cs
@@ -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);
+ }
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/Reflection/OriginalInterfaceProxyFactory.cs b/src/SMAPI/Framework/Reflection/OriginalInterfaceProxyFactory.cs
new file mode 100644
index 00000000..d6966978
--- /dev/null
+++ b/src/SMAPI/Framework/Reflection/OriginalInterfaceProxyFactory.cs
@@ -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);
+ }
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs
index b3c9087f..44853627 100644
--- a/src/SMAPI/Framework/SCore.cs
+++ b/src/SMAPI/Framework/SCore.cs
@@ -1560,7 +1560,9 @@ namespace StardewModdingAPI.Framework
{
// init
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
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="errorDetails">More detailed details about the error intended for developers (if any).</param>
/// <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;