Merge branch 'feature/mod-provided-apis' into develop
This commit is contained in:
commit
6a4dc7e7d1
|
@ -1,6 +1,8 @@
|
|||
# Release notes
|
||||
## 2.3
|
||||
* For modders:
|
||||
* **Added mod-provided APIs** which enable simple integrations between mods, even without direct assembly references.
|
||||
* Added `GameEvents.FirstUpdateTick` event, which is called once after all mods are initialised.
|
||||
* Added `IsSuppressed` to input events so mods can optionally avoid handling a key another mod already handled.
|
||||
* Added trace message listing mods with no update keys.
|
||||
* Fixed `GraphicsEvents.OnPostRenderEvent` not being raised in some specialised cases.
|
||||
|
|
|
@ -41,10 +41,6 @@
|
|||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
|
|
@ -33,6 +33,9 @@ namespace StardewModdingAPI.Events
|
|||
/// <summary>Raised every 60th tick (≈once per second).</summary>
|
||||
public static event EventHandler OneSecondTick;
|
||||
|
||||
/// <summary>Raised once after the game initialises and all <see cref="IMod.Entry"/> methods have been called.</summary>
|
||||
public static event EventHandler FirstUpdateTick;
|
||||
|
||||
|
||||
/*********
|
||||
** Internal methods
|
||||
|
@ -92,5 +95,12 @@ namespace StardewModdingAPI.Events
|
|||
{
|
||||
monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.OneSecondTick)}", GameEvents.OneSecondTick?.GetInvocationList());
|
||||
}
|
||||
|
||||
/// <summary>Raise a <see cref="FirstUpdateTick"/> event.</summary>
|
||||
/// <param name="monitor">Encapsulates monitoring and logging.</param>
|
||||
internal static void InvokeFirstUpdateTick(IMonitor monitor)
|
||||
{
|
||||
monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.FirstUpdateTick)}", GameEvents.FirstUpdateTick?.GetInvocationList());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>Get metadata for all loaded mods.</summary>
|
||||
public IEnumerable<IManifest> GetAll()
|
||||
/// <summary>Register a mod as a possible source of deprecation warnings.</summary>
|
||||
/// <param name="metadata">The mod metadata.</param>
|
||||
public void Add(IModMetadata metadata)
|
||||
{
|
||||
return this.Mods.Select(p => p.Manifest);
|
||||
this.Mods.Add(metadata);
|
||||
this.ModNamesByAssembly[metadata.Mod.GetType().Assembly.FullName] = metadata;
|
||||
}
|
||||
|
||||
/// <summary>Get metadata for all loaded mods.</summary>
|
||||
public IEnumerable<IModMetadata> GetAll()
|
||||
{
|
||||
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));
|
||||
return this.GetAll().FirstOrDefault(p => p.Manifest.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);
|
||||
}
|
||||
|
||||
/// <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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
namespace StardewModdingAPI
|
||||
namespace StardewModdingAPI
|
||||
{
|
||||
/// <summary>The implementation for a Stardew Valley mod.</summary>
|
||||
public interface IMod
|
||||
|
@ -22,5 +22,8 @@
|
|||
/// <summary>The mod entry point, called after the mod is first loaded.</summary>
|
||||
/// <param name="helper">Provides simplified APIs for writing mods.</param>
|
||||
void Entry(IModHelper helper);
|
||||
|
||||
/// <summary>Get an API that other mods can access. This is always called after <see cref="Entry"/>.</summary>
|
||||
object GetApi();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace StardewModdingAPI
|
||||
{
|
||||
|
@ -16,5 +16,14 @@ namespace StardewModdingAPI
|
|||
/// <summary>Get whether a mod has been loaded.</summary>
|
||||
/// <param name="uniqueID">The mod's unique ID.</param>
|
||||
bool IsLoaded(string uniqueID);
|
||||
|
||||
/// <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>
|
||||
/// <param name="uniqueID">The mod's unique ID.</param>
|
||||
object GetApi(string uniqueID);
|
||||
|
||||
/// <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>
|
||||
TInterface GetApi<TInterface>(string uniqueID) where TInterface : class;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,7 +50,6 @@ namespace StardewModdingAPI.Metadata
|
|||
new EventFinder("StardewModdingAPI.Events.GameEvents", "Initialize", InstructionHandleResult.NotCompatible),
|
||||
new EventFinder("StardewModdingAPI.Events.GameEvents", "LoadContent", InstructionHandleResult.NotCompatible),
|
||||
new EventFinder("StardewModdingAPI.Events.GameEvents", "GameLoaded", InstructionHandleResult.NotCompatible),
|
||||
new EventFinder("StardewModdingAPI.Events.GameEvents", "FirstUpdateTick", InstructionHandleResult.NotCompatible),
|
||||
new EventFinder("StardewModdingAPI.Events.PlayerEvents", "LoadedGame", InstructionHandleResult.NotCompatible),
|
||||
new EventFinder("StardewModdingAPI.Events.PlayerEvents", "FarmerChanged", InstructionHandleResult.NotCompatible),
|
||||
new EventFinder("StardewModdingAPI.Events.TimeEvents", "DayOfMonthChanged", InstructionHandleResult.NotCompatible),
|
||||
|
|
|
@ -25,6 +25,9 @@ namespace StardewModdingAPI
|
|||
/// <param name="helper">Provides simplified APIs for writing mods.</param>
|
||||
public abstract void Entry(IModHelper helper);
|
||||
|
||||
/// <summary>Get an API that other mods can access. This is always called after <see cref="Entry"/>.</summary>
|
||||
public virtual object GetApi() => null;
|
||||
|
||||
/// <summary>Release or reset unmanaged resources.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
|
|
|
@ -247,7 +247,7 @@ namespace StardewModdingAPI
|
|||
this.IsDisposed = true;
|
||||
|
||||
// dispose mod data
|
||||
foreach (IModMetadata mod in this.ModRegistry.GetMods())
|
||||
foreach (IModMetadata mod in this.ModRegistry.GetAll())
|
||||
{
|
||||
try
|
||||
{
|
||||
|
@ -374,7 +374,7 @@ namespace StardewModdingAPI
|
|||
}
|
||||
|
||||
// update window titles
|
||||
int modsLoaded = this.ModRegistry.GetMods().Count();
|
||||
int modsLoaded = this.ModRegistry.GetAll().Count();
|
||||
this.GameInstance.Window.Title = $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion} with {modsLoaded} mods";
|
||||
Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion} with {modsLoaded} mods";
|
||||
|
||||
|
@ -390,7 +390,7 @@ namespace StardewModdingAPI
|
|||
LocalizedContentManager.LanguageCode languageCode = this.ContentManager.GetCurrentLanguage();
|
||||
|
||||
// update mod translation helpers
|
||||
foreach (IModMetadata mod in this.ModRegistry.GetMods())
|
||||
foreach (IModMetadata mod in this.ModRegistry.GetAll())
|
||||
(mod.Mod.Helper.Translation as TranslationHelper)?.SetLocale(locale, languageCode);
|
||||
}
|
||||
|
||||
|
@ -655,6 +655,7 @@ namespace StardewModdingAPI
|
|||
|
||||
AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.TargetPlatform, this.Monitor, this.Settings.DeveloperMode);
|
||||
AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => modAssemblyLoader.ResolveAssembly(e.Name);
|
||||
InterfaceProxyBuilder proxyBuilder = new InterfaceProxyBuilder();
|
||||
foreach (IModMetadata metadata in mods)
|
||||
{
|
||||
// get basic info
|
||||
|
@ -696,53 +697,30 @@ namespace StardewModdingAPI
|
|||
continue;
|
||||
}
|
||||
|
||||
// validate assembly
|
||||
try
|
||||
{
|
||||
int modEntries = modAssembly.DefinedTypes.Count(type => typeof(Mod).IsAssignableFrom(type) && !type.IsAbstract);
|
||||
if (modEntries == 0)
|
||||
{
|
||||
TrackSkip(metadata, $"its DLL has no '{nameof(Mod)}' subclass.");
|
||||
continue;
|
||||
}
|
||||
if (modEntries > 1)
|
||||
{
|
||||
TrackSkip(metadata, $"its DLL contains multiple '{nameof(Mod)}' subclasses.");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
TrackSkip(metadata, $"its DLL couldn't be loaded:\n{ex.GetLogSummary()}");
|
||||
continue;
|
||||
}
|
||||
|
||||
// initialise mod
|
||||
try
|
||||
{
|
||||
// get implementation
|
||||
TypeInfo modEntryType = modAssembly.DefinedTypes.First(type => typeof(Mod).IsAssignableFrom(type) && !type.IsAbstract);
|
||||
Mod mod = (Mod)modAssembly.CreateInstance(modEntryType.ToString());
|
||||
if (mod == null)
|
||||
// init mod helpers
|
||||
IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName);
|
||||
IModHelper modHelper;
|
||||
{
|
||||
TrackSkip(metadata, "its entry class couldn't be instantiated.");
|
||||
continue;
|
||||
}
|
||||
|
||||
// inject data
|
||||
{
|
||||
IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName);
|
||||
ICommandHelper commandHelper = new CommandHelper(manifest.UniqueID, metadata.DisplayName, this.CommandManager);
|
||||
IContentHelper contentHelper = new ContentHelper(contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor);
|
||||
IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, metadata.DisplayName, this.Reflection, this.DeprecationManager);
|
||||
IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry);
|
||||
IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry, proxyBuilder, monitor);
|
||||
ITranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentManager.GetLocale(), contentManager.GetCurrentLanguage());
|
||||
|
||||
mod.ModManifest = manifest;
|
||||
mod.Helper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, translationHelper);
|
||||
mod.Monitor = monitor;
|
||||
modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, translationHelper);
|
||||
}
|
||||
|
||||
// get mod instance
|
||||
if (!this.TryLoadModEntry(modAssembly, error => TrackSkip(metadata, error), out Mod mod))
|
||||
continue;
|
||||
|
||||
// init mod
|
||||
mod.ModManifest = manifest;
|
||||
mod.Helper = modHelper;
|
||||
mod.Monitor = monitor;
|
||||
|
||||
// track mod
|
||||
metadata.SetMod(mod);
|
||||
this.ModRegistry.Add(metadata);
|
||||
|
@ -753,7 +731,7 @@ namespace StardewModdingAPI
|
|||
}
|
||||
}
|
||||
}
|
||||
IModMetadata[] loadedMods = this.ModRegistry.GetMods().ToArray();
|
||||
IModMetadata[] loadedMods = this.ModRegistry.GetAll().ToArray();
|
||||
|
||||
// log skipped mods
|
||||
this.Monitor.Newline();
|
||||
|
@ -817,6 +795,19 @@ namespace StardewModdingAPI
|
|||
{
|
||||
this.Monitor.Log($"{metadata.DisplayName} failed on entry and might not work correctly. Technical details:\n{ex.GetLogSummary()}", LogLevel.Error);
|
||||
}
|
||||
|
||||
// get mod API
|
||||
try
|
||||
{
|
||||
object api = metadata.Mod.GetApi();
|
||||
if (api != null)
|
||||
this.Monitor.Log($" Found mod-provided API ({api.GetType().FullName}).", LogLevel.Trace);
|
||||
metadata.SetApi(api);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.Monitor.Log($"Failed loading mod-provided API for {metadata.DisplayName}. Integrations with other mods may not work. Error: {ex.GetLogSummary()}", LogLevel.Error);
|
||||
}
|
||||
}
|
||||
|
||||
// invalidate cache entries when needed
|
||||
|
@ -852,13 +843,48 @@ namespace StardewModdingAPI
|
|||
this.Monitor.Log("Invalidating cached assets for new editors & loaders...", LogLevel.Trace);
|
||||
this.ContentManager.InvalidateCacheFor(editors, loaders);
|
||||
}
|
||||
|
||||
// unlock mod integrations
|
||||
this.ModRegistry.AreAllModsInitialised = true;
|
||||
}
|
||||
|
||||
/// <summary>Load a mod's entry class.</summary>
|
||||
/// <param name="modAssembly">The mod assembly.</param>
|
||||
/// <param name="onError">A callback invoked when loading fails.</param>
|
||||
/// <param name="mod">The loaded instance.</param>
|
||||
private bool TryLoadModEntry(Assembly modAssembly, Action<string> onError, out Mod mod)
|
||||
{
|
||||
mod = null;
|
||||
|
||||
// find type
|
||||
TypeInfo[] modEntries = modAssembly.DefinedTypes.Where(type => typeof(Mod).IsAssignableFrom(type) && !type.IsAbstract).Take(2).ToArray();
|
||||
if (modEntries.Length == 0)
|
||||
{
|
||||
onError($"its DLL has no '{nameof(Mod)}' subclass.");
|
||||
return false;
|
||||
}
|
||||
if (modEntries.Length > 1)
|
||||
{
|
||||
onError($"its DLL contains multiple '{nameof(Mod)}' subclasses.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// get implementation
|
||||
mod = (Mod)modAssembly.CreateInstance(modEntries[0].ToString());
|
||||
if (mod == null)
|
||||
{
|
||||
onError("its entry class couldn't be instantiated.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Reload translations for all mods.</summary>
|
||||
private void ReloadTranslations()
|
||||
{
|
||||
JsonHelper jsonHelper = new JsonHelper();
|
||||
foreach (IModMetadata metadata in this.ModRegistry.GetMods())
|
||||
foreach (IModMetadata metadata in this.ModRegistry.GetAll())
|
||||
{
|
||||
// read translation files
|
||||
IDictionary<string, IDictionary<string, string>> translations = new Dictionary<string, IDictionary<string, string>>();
|
||||
|
|
|
@ -79,10 +79,6 @@
|
|||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="System.Windows.Forms" Condition="$(OS) == 'Windows_NT'" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
<Reference Include="System.Data.DataSetExtensions" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
@ -110,6 +106,7 @@
|
|||
<Compile Include="Framework\ContentManagerShim.cs" />
|
||||
<Compile Include="Framework\Exceptions\SAssemblyLoadFailedException.cs" />
|
||||
<Compile Include="Framework\ModLoading\AssemblyLoadStatus.cs" />
|
||||
<Compile Include="Framework\Reflection\InterfaceProxyBuilder.cs" />
|
||||
<Compile Include="Framework\Utilities\ContextHash.cs" />
|
||||
<Compile Include="IReflectedField.cs" />
|
||||
<Compile Include="IReflectedMethod.cs" />
|
||||
|
|
Loading…
Reference in New Issue