summaryrefslogtreecommitdiff
path: root/src/SMAPI/Framework
diff options
context:
space:
mode:
authorJesse Plamondon-Willard <github@jplamondonw.com>2017-12-16 00:31:31 -0500
committerJesse Plamondon-Willard <github@jplamondonw.com>2017-12-16 00:31:31 -0500
commit6a4dc7e7d1dddb54264856102540d205bc6c3509 (patch)
treedb53e648d6ce2f155e19d307bb60abbd14e8ca96 /src/SMAPI/Framework
parent69c9ab0ecd184e4706a8e6394b38fa592cb808d0 (diff)
parent4c3ae950c6be0840e98d1328eeca9effca9c3807 (diff)
downloadSMAPI-6a4dc7e7d1dddb54264856102540d205bc6c3509.tar.gz
SMAPI-6a4dc7e7d1dddb54264856102540d205bc6c3509.tar.bz2
SMAPI-6a4dc7e7d1dddb54264856102540d205bc6c3509.zip
Merge branch 'feature/mod-provided-apis' into develop
Diffstat (limited to 'src/SMAPI/Framework')
-rw-r--r--src/SMAPI/Framework/DeprecationManager.cs4
-rw-r--r--src/SMAPI/Framework/IModMetadata.cs7
-rw-r--r--src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs67
-rw-r--r--src/SMAPI/Framework/ModLoading/ModMetadata.cs11
-rw-r--r--src/SMAPI/Framework/ModRegistry.cs62
-rw-r--r--src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs138
-rw-r--r--src/SMAPI/Framework/SGame.cs13
7 files changed, 251 insertions, 51 deletions
diff --git a/src/SMAPI/Framework/DeprecationManager.cs b/src/SMAPI/Framework/DeprecationManager.cs
index 20bb0d2d..7a824a05 100644
--- a/src/SMAPI/Framework/DeprecationManager.cs
+++ b/src/SMAPI/Framework/DeprecationManager.cs
@@ -37,7 +37,7 @@ namespace StardewModdingAPI.Framework
/// <param name="severity">How deprecated the code is.</param>
public void Warn(string nounPhrase, string version, DeprecationLevel severity)
{
- this.Warn(this.ModRegistry.GetModFromStack(), nounPhrase, version, severity);
+ this.Warn(this.ModRegistry.GetFromStack()?.DisplayName, nounPhrase, version, severity);
}
/// <summary>Log a deprecation warning.</summary>
@@ -82,7 +82,7 @@ namespace StardewModdingAPI.Framework
/// <returns>Returns whether the deprecation was successfully marked as warned. Returns <c>false</c> if it was already marked.</returns>
public bool MarkWarned(string nounPhrase, string version)
{
- return this.MarkWarned(this.ModRegistry.GetModFromStack(), nounPhrase, version);
+ return this.MarkWarned(this.ModRegistry.GetFromStack()?.DisplayName, nounPhrase, version);
}
/// <summary>Mark a deprecation warning as already logged.</summary>
diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs
index c21734a7..a36994fd 100644
--- a/src/SMAPI/Framework/IModMetadata.cs
+++ b/src/SMAPI/Framework/IModMetadata.cs
@@ -30,6 +30,9 @@ namespace StardewModdingAPI.Framework
/// <summary>The mod instance (if it was loaded).</summary>
IMod Mod { get; }
+ /// <summary>The mod-provided API (if any).</summary>
+ object Api { get; }
+
/*********
** Public methods
@@ -43,5 +46,9 @@ namespace StardewModdingAPI.Framework
/// <summary>Set the mod instance.</summary>
/// <param name="mod">The mod instance to set.</param>
IModMetadata SetMod(IMod mod);
+
+ /// <summary>Set the mod-provided API instance.</summary>
+ /// <param name="api">The mod-provided API.</param>
+ IModMetadata SetApi(object api);
}
}
diff --git a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs
index 9e824694..ea0dbb38 100644
--- a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs
@@ -1,4 +1,6 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
+using System.Linq;
+using StardewModdingAPI.Framework.Reflection;
namespace StardewModdingAPI.Framework.ModHelpers
{
@@ -11,6 +13,15 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <summary>The underlying mod registry.</summary>
private readonly ModRegistry Registry;
+ /// <summary>Encapsulates monitoring and logging for the mod.</summary>
+ private readonly IMonitor Monitor;
+
+ /// <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
@@ -18,16 +29,20 @@ 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>
- public ModRegistryHelper(string modID, ModRegistry registry)
+ /// <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, InterfaceProxyBuilder proxyBuilder, IMonitor monitor)
: base(modID)
{
this.Registry = registry;
+ this.ProxyBuilder = proxyBuilder;
+ this.Monitor = monitor;
}
/// <summary>Get metadata for all loaded mods.</summary>
public IEnumerable<IManifest> GetAll()
{
- return this.Registry.GetAll();
+ return this.Registry.GetAll().Select(p => p.Manifest);
}
/// <summary>Get metadata for a loaded mod.</summary>
@@ -35,14 +50,56 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <returns>Returns the matching mod's metadata, or <c>null</c> if not found.</returns>
public IManifest Get(string uniqueID)
{
- return this.Registry.Get(uniqueID);
+ return this.Registry.Get(uniqueID)?.Manifest;
}
/// <summary>Get whether a mod has been loaded.</summary>
/// <param name="uniqueID">The mod's unique ID.</param>
public bool IsLoaded(string uniqueID)
{
- return this.Registry.IsLoaded(uniqueID);
+ return this.Registry.Get(uniqueID) != null;
+ }
+
+ /// <summary>Get the API provided by a mod, or <c>null</c> if it has none. This signature requires using the <see cref="IModHelper.Reflection"/> API to access the API's properties and methods.</summary>
+ public object GetApi(string uniqueID)
+ {
+ IModMetadata mod = this.Registry.Get(uniqueID);
+ if (mod?.Api != null && this.AccessedModApis.Add(mod.Manifest.UniqueID))
+ this.Monitor.Log($"Accessed mod-provided API for {mod.DisplayName}.", LogLevel.Trace);
+ return mod?.Api;
+ }
+
+ /// <summary>Get the API provided by a mod, mapped to a given interface which specifies the expected properties and methods. If the mod has no API or it's not compatible with the given interface, get <c>null</c>.</summary>
+ /// <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>
+ public TInterface GetApi<TInterface>(string uniqueID) where TInterface : class
+ {
+ // validate
+ if (!this.Registry.AreAllModsInitialised)
+ {
+ this.Monitor.Log("Tried to access a mod-provided API before all mods were initialised.", LogLevel.Error);
+ return null;
+ }
+ if (!typeof(TInterface).IsInterface)
+ {
+ this.Monitor.Log("Tried to map a mod-provided API to a class; must be a public interface.", LogLevel.Error);
+ return null;
+ }
+ if (!typeof(TInterface).IsPublic)
+ {
+ this.Monitor.Log("Tried to map a mod-provided API to a non-public interface; must be a public interface.", LogLevel.Error);
+ return null;
+ }
+
+ // get raw API
+ object api = this.GetApi(uniqueID);
+ if (api == null)
+ return null;
+
+ // get API of type
+ if (api is TInterface castApi)
+ return castApi;
+ return this.ProxyBuilder.CreateProxy<TInterface>(api, this.ModID, uniqueID);
}
}
}
diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs
index 5055da75..30fe211b 100644
--- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs
+++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs
@@ -29,6 +29,9 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <summary>The mod instance (if it was loaded).</summary>
public IMod Mod { get; private set; }
+ /// <summary>The mod-provided API (if any).</summary>
+ public object Api { get; private set; }
+
/*********
** Public methods
@@ -64,5 +67,13 @@ namespace StardewModdingAPI.Framework.ModLoading
this.Mod = mod;
return this;
}
+
+ /// <summary>Set the mod-provided API instance.</summary>
+ /// <param name="api">The mod-provided API.</param>
+ public IModMetadata SetApi(object api)
+ {
+ this.Api = api;
+ return this;
+ }
}
}
diff --git a/src/SMAPI/Framework/ModRegistry.cs b/src/SMAPI/Framework/ModRegistry.cs
index 9dde7a20..453d2868 100644
--- a/src/SMAPI/Framework/ModRegistry.cs
+++ b/src/SMAPI/Framework/ModRegistry.cs
@@ -15,26 +15,34 @@ namespace StardewModdingAPI.Framework
/// <summary>The registered mod data.</summary>
private readonly List<IModMetadata> Mods = new List<IModMetadata>();
- /// <summary>The friendly mod names treated as deprecation warning sources (assembly full name => mod name).</summary>
- private readonly IDictionary<string, string> ModNamesByAssembly = new Dictionary<string, string>();
+ /// <summary>An assembly full name => mod lookup.</summary>
+ private readonly IDictionary<string, IModMetadata> ModNamesByAssembly = new Dictionary<string, IModMetadata>();
+
+ /// <summary>Whether all mods have been initialised and their <see cref="IMod.Entry"/> method called.</summary>
+ public bool AreAllModsInitialised { get; set; }
/*********
** Public methods
*********/
- /****
- ** Basic metadata
- ****/
+ /// <summary>Register a mod as a possible source of deprecation warnings.</summary>
+ /// <param name="metadata">The mod metadata.</param>
+ public void Add(IModMetadata metadata)
+ {
+ this.Mods.Add(metadata);
+ this.ModNamesByAssembly[metadata.Mod.GetType().Assembly.FullName] = metadata;
+ }
+
/// <summary>Get metadata for all loaded mods.</summary>
- public IEnumerable<IManifest> GetAll()
+ public IEnumerable<IModMetadata> GetAll()
{
- return this.Mods.Select(p => p.Manifest);
+ return this.Mods.Select(p => p);
}
/// <summary>Get metadata for a loaded mod.</summary>
/// <param name="uniqueID">The mod's unique ID.</param>
/// <returns>Returns the matching mod's metadata, or <c>null</c> if not found.</returns>
- public IManifest Get(string uniqueID)
+ public IModMetadata Get(string uniqueID)
{
// normalise search ID
if (string.IsNullOrWhiteSpace(uniqueID))
@@ -42,37 +50,13 @@ namespace StardewModdingAPI.Framework
uniqueID = uniqueID.Trim();
// find match
- return this.GetAll().FirstOrDefault(p => p.UniqueID.Trim().Equals(uniqueID, StringComparison.InvariantCultureIgnoreCase));
- }
-
- /// <summary>Get whether a mod has been loaded.</summary>
- /// <param name="uniqueID">The mod's unique ID.</param>
- public bool IsLoaded(string uniqueID)
- {
- return this.Get(uniqueID) != null;
- }
-
- /****
- ** Mod data
- ****/
- /// <summary>Register a mod as a possible source of deprecation warnings.</summary>
- /// <param name="metadata">The mod metadata.</param>
- public void Add(IModMetadata metadata)
- {
- this.Mods.Add(metadata);
- this.ModNamesByAssembly[metadata.Mod.GetType().Assembly.FullName] = metadata.DisplayName;
- }
-
- /// <summary>Get all enabled mods.</summary>
- public IEnumerable<IModMetadata> GetMods()
- {
- return (from mod in this.Mods select mod);
+ return this.GetAll().FirstOrDefault(p => p.Manifest.UniqueID.Trim().Equals(uniqueID, StringComparison.InvariantCultureIgnoreCase));
}
- /// <summary>Get the friendly mod name which defines a type.</summary>
+ /// <summary>Get the mod metadata from one of its assemblies.</summary>
/// <param name="type">The type to check.</param>
/// <returns>Returns the mod name, or <c>null</c> if the type isn't part of a known mod.</returns>
- public string GetModFrom(Type type)
+ public IModMetadata GetFrom(Type type)
{
// null
if (type == null)
@@ -89,7 +73,7 @@ namespace StardewModdingAPI.Framework
/// <summary>Get the friendly name for the closest assembly registered as a source of deprecation warnings.</summary>
/// <returns>Returns the source name, or <c>null</c> if no registered assemblies were found.</returns>
- public string GetModFromStack()
+ public IModMetadata GetFromStack()
{
// get stack frames
StackTrace stack = new StackTrace();
@@ -101,9 +85,9 @@ namespace StardewModdingAPI.Framework
foreach (StackFrame frame in frames)
{
MethodBase method = frame.GetMethod();
- string name = this.GetModFrom(method.ReflectedType);
- if (name != null)
- return name;
+ IModMetadata mod = this.GetFrom(method.ReflectedType);
+ if (mod != null)
+ return mod;
}
// no known assembly found
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/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs
index e9777e0b..0a614f17 100644
--- a/src/SMAPI/Framework/SGame.cs
+++ b/src/SMAPI/Framework/SGame.cs
@@ -557,9 +557,12 @@ namespace StardewModdingAPI.Framework
/*********
** Update events
*********/
- GameEvents.InvokeUpdateTick(this.Monitor);
if (this.FirstUpdate)
+ {
this.FirstUpdate = false;
+ GameEvents.InvokeFirstUpdateTick(this.Monitor);
+ }
+ GameEvents.InvokeUpdateTick(this.Monitor);
if (this.CurrentUpdateTick % 2 == 0)
GameEvents.InvokeSecondUpdateTick(this.Monitor);
if (this.CurrentUpdateTick % 4 == 0)
@@ -725,7 +728,7 @@ namespace StardewModdingAPI.Framework
}
if (Game1.overlayMenu != null)
{
- Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState) null, (RasterizerState) null);
+ Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
Game1.overlayMenu.draw(Game1.spriteBatch);
Game1.spriteBatch.End();
}
@@ -759,7 +762,7 @@ namespace StardewModdingAPI.Framework
}
if (Game1.overlayMenu != null)
{
- Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState) null, (RasterizerState) null);
+ Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
Game1.overlayMenu.draw(Game1.spriteBatch);
Game1.spriteBatch.End();
}
@@ -793,7 +796,7 @@ namespace StardewModdingAPI.Framework
}
if (Game1.overlayMenu != null)
{
- Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState) null, (RasterizerState) null);
+ Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
Game1.overlayMenu.draw(Game1.spriteBatch);
Game1.spriteBatch.End();
}
@@ -826,7 +829,7 @@ namespace StardewModdingAPI.Framework
}
if (Game1.overlayMenu != null)
{
- Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState) null, (RasterizerState) null);
+ Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
Game1.overlayMenu.draw(Game1.spriteBatch);
Game1.spriteBatch.End();
}