Merge branch 'feature/mod-provided-apis' into develop

This commit is contained in:
Jesse Plamondon-Willard 2017-12-16 00:31:31 -05:00
commit 6a4dc7e7d1
16 changed files with 352 additions and 107 deletions

View File

@ -1,6 +1,8 @@
# Release notes # Release notes
## 2.3 ## 2.3
* For modders: * 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 `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. * Added trace message listing mods with no update keys.
* Fixed `GraphicsEvents.OnPostRenderEvent` not being raised in some specialised cases. * Fixed `GraphicsEvents.OnPostRenderEvent` not being raised in some specialised cases.

View File

@ -41,10 +41,6 @@
<Private>False</Private> <Private>False</Private>
</Reference> </Reference>
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" /> <Reference Include="System.Xml" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -33,6 +33,9 @@ namespace StardewModdingAPI.Events
/// <summary>Raised every 60th tick (≈once per second).</summary> /// <summary>Raised every 60th tick (≈once per second).</summary>
public static event EventHandler OneSecondTick; 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 ** Internal methods
@ -92,5 +95,12 @@ namespace StardewModdingAPI.Events
{ {
monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.OneSecondTick)}", GameEvents.OneSecondTick?.GetInvocationList()); 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());
}
} }
} }

View File

@ -37,7 +37,7 @@ namespace StardewModdingAPI.Framework
/// <param name="severity">How deprecated the code is.</param> /// <param name="severity">How deprecated the code is.</param>
public void Warn(string nounPhrase, string version, DeprecationLevel severity) 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> /// <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> /// <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) 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> /// <summary>Mark a deprecation warning as already logged.</summary>

View File

@ -30,6 +30,9 @@ namespace StardewModdingAPI.Framework
/// <summary>The mod instance (if it was loaded).</summary> /// <summary>The mod instance (if it was loaded).</summary>
IMod Mod { get; } IMod Mod { get; }
/// <summary>The mod-provided API (if any).</summary>
object Api { get; }
/********* /*********
** Public methods ** Public methods
@ -43,5 +46,9 @@ namespace StardewModdingAPI.Framework
/// <summary>Set the mod instance.</summary> /// <summary>Set the mod instance.</summary>
/// <param name="mod">The mod instance to set.</param> /// <param name="mod">The mod instance to set.</param>
IModMetadata SetMod(IMod mod); IModMetadata SetMod(IMod mod);
/// <summary>Set the mod-provided API instance.</summary>
/// <param name="api">The mod-provided API.</param>
IModMetadata SetApi(object api);
} }
} }

View File

@ -1,4 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using StardewModdingAPI.Framework.Reflection;
namespace StardewModdingAPI.Framework.ModHelpers namespace StardewModdingAPI.Framework.ModHelpers
{ {
@ -11,6 +13,15 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <summary>The underlying mod registry.</summary> /// <summary>The underlying mod registry.</summary>
private readonly ModRegistry Registry; 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 ** Public methods
@ -18,16 +29,20 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <summary>Construct an instance.</summary> /// <summary>Construct an instance.</summary>
/// <param name="modID">The unique ID of the relevant mod.</param> /// <param name="modID">The unique ID of the relevant mod.</param>
/// <param name="registry">The underlying mod registry.</param> /// <param name="registry">The underlying mod registry.</param>
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) : base(modID)
{ {
this.Registry = registry; this.Registry = registry;
this.ProxyBuilder = proxyBuilder;
this.Monitor = monitor;
} }
/// <summary>Get metadata for all loaded mods.</summary> /// <summary>Get metadata for all loaded mods.</summary>
public IEnumerable<IManifest> GetAll() public IEnumerable<IManifest> GetAll()
{ {
return this.Registry.GetAll(); return this.Registry.GetAll().Select(p => p.Manifest);
} }
/// <summary>Get metadata for a loaded mod.</summary> /// <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> /// <returns>Returns the matching mod's metadata, or <c>null</c> if not found.</returns>
public IManifest Get(string uniqueID) 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> /// <summary>Get whether a mod has been loaded.</summary>
/// <param name="uniqueID">The mod's unique ID.</param> /// <param name="uniqueID">The mod's unique ID.</param>
public bool IsLoaded(string uniqueID) 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);
} }
} }
} }

View File

@ -29,6 +29,9 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <summary>The mod instance (if it was loaded).</summary> /// <summary>The mod instance (if it was loaded).</summary>
public IMod Mod { get; private set; } public IMod Mod { get; private set; }
/// <summary>The mod-provided API (if any).</summary>
public object Api { get; private set; }
/********* /*********
** Public methods ** Public methods
@ -64,5 +67,13 @@ namespace StardewModdingAPI.Framework.ModLoading
this.Mod = mod; this.Mod = mod;
return this; 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;
}
} }
} }

View File

@ -15,26 +15,34 @@ namespace StardewModdingAPI.Framework
/// <summary>The registered mod data.</summary> /// <summary>The registered mod data.</summary>
private readonly List<IModMetadata> Mods = new List<IModMetadata>(); private readonly List<IModMetadata> Mods = new List<IModMetadata>();
/// <summary>The friendly mod names treated as deprecation warning sources (assembly full name => mod name).</summary> /// <summary>An assembly full name => mod lookup.</summary>
private readonly IDictionary<string, string> ModNamesByAssembly = new Dictionary<string, string>(); 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 ** Public methods
*********/ *********/
/**** /// <summary>Register a mod as a possible source of deprecation warnings.</summary>
** Basic metadata /// <param name="metadata">The mod metadata.</param>
****/ public void Add(IModMetadata metadata)
/// <summary>Get metadata for all loaded mods.</summary>
public IEnumerable<IManifest> GetAll()
{ {
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> /// <summary>Get metadata for a loaded mod.</summary>
/// <param name="uniqueID">The mod's unique ID.</param> /// <param name="uniqueID">The mod's unique ID.</param>
/// <returns>Returns the matching mod's metadata, or <c>null</c> if not found.</returns> /// <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 // normalise search ID
if (string.IsNullOrWhiteSpace(uniqueID)) if (string.IsNullOrWhiteSpace(uniqueID))
@ -42,37 +50,13 @@ namespace StardewModdingAPI.Framework
uniqueID = uniqueID.Trim(); uniqueID = uniqueID.Trim();
// find match // 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> /// <summary>Get the mod metadata from one of its assemblies.</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>
/// <param name="type">The type to check.</param> /// <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> /// <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 // null
if (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> /// <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> /// <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 // get stack frames
StackTrace stack = new StackTrace(); StackTrace stack = new StackTrace();
@ -101,9 +85,9 @@ namespace StardewModdingAPI.Framework
foreach (StackFrame frame in frames) foreach (StackFrame frame in frames)
{ {
MethodBase method = frame.GetMethod(); MethodBase method = frame.GetMethod();
string name = this.GetModFrom(method.ReflectedType); IModMetadata mod = this.GetFrom(method.ReflectedType);
if (name != null) if (mod != null)
return name; return mod;
} }
// no known assembly found // no known assembly found

View File

@ -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);
}
}
}
}

View File

@ -557,9 +557,12 @@ namespace StardewModdingAPI.Framework
/********* /*********
** Update events ** Update events
*********/ *********/
GameEvents.InvokeUpdateTick(this.Monitor);
if (this.FirstUpdate) if (this.FirstUpdate)
{
this.FirstUpdate = false; this.FirstUpdate = false;
GameEvents.InvokeFirstUpdateTick(this.Monitor);
}
GameEvents.InvokeUpdateTick(this.Monitor);
if (this.CurrentUpdateTick % 2 == 0) if (this.CurrentUpdateTick % 2 == 0)
GameEvents.InvokeSecondUpdateTick(this.Monitor); GameEvents.InvokeSecondUpdateTick(this.Monitor);
if (this.CurrentUpdateTick % 4 == 0) if (this.CurrentUpdateTick % 4 == 0)

View File

@ -1,4 +1,4 @@
namespace StardewModdingAPI namespace StardewModdingAPI
{ {
/// <summary>The implementation for a Stardew Valley mod.</summary> /// <summary>The implementation for a Stardew Valley mod.</summary>
public interface IMod public interface IMod
@ -22,5 +22,8 @@
/// <summary>The mod entry point, called after the mod is first loaded.</summary> /// <summary>The mod entry point, called after the mod is first loaded.</summary>
/// <param name="helper">Provides simplified APIs for writing mods.</param> /// <param name="helper">Provides simplified APIs for writing mods.</param>
void Entry(IModHelper helper); void Entry(IModHelper helper);
/// <summary>Get an API that other mods can access. This is always called after <see cref="Entry"/>.</summary>
object GetApi();
} }
} }

View File

@ -1,4 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
namespace StardewModdingAPI namespace StardewModdingAPI
{ {
@ -16,5 +16,14 @@ namespace StardewModdingAPI
/// <summary>Get whether a mod has been loaded.</summary> /// <summary>Get whether a mod has been loaded.</summary>
/// <param name="uniqueID">The mod's unique ID.</param> /// <param name="uniqueID">The mod's unique ID.</param>
bool IsLoaded(string uniqueID); 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;
} }
} }

View File

@ -50,7 +50,6 @@ namespace StardewModdingAPI.Metadata
new EventFinder("StardewModdingAPI.Events.GameEvents", "Initialize", InstructionHandleResult.NotCompatible), new EventFinder("StardewModdingAPI.Events.GameEvents", "Initialize", InstructionHandleResult.NotCompatible),
new EventFinder("StardewModdingAPI.Events.GameEvents", "LoadContent", InstructionHandleResult.NotCompatible), new EventFinder("StardewModdingAPI.Events.GameEvents", "LoadContent", InstructionHandleResult.NotCompatible),
new EventFinder("StardewModdingAPI.Events.GameEvents", "GameLoaded", 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", "LoadedGame", InstructionHandleResult.NotCompatible),
new EventFinder("StardewModdingAPI.Events.PlayerEvents", "FarmerChanged", InstructionHandleResult.NotCompatible), new EventFinder("StardewModdingAPI.Events.PlayerEvents", "FarmerChanged", InstructionHandleResult.NotCompatible),
new EventFinder("StardewModdingAPI.Events.TimeEvents", "DayOfMonthChanged", InstructionHandleResult.NotCompatible), new EventFinder("StardewModdingAPI.Events.TimeEvents", "DayOfMonthChanged", InstructionHandleResult.NotCompatible),

View File

@ -25,6 +25,9 @@ namespace StardewModdingAPI
/// <param name="helper">Provides simplified APIs for writing mods.</param> /// <param name="helper">Provides simplified APIs for writing mods.</param>
public abstract void Entry(IModHelper helper); 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> /// <summary>Release or reset unmanaged resources.</summary>
public void Dispose() public void Dispose()
{ {

View File

@ -247,7 +247,7 @@ namespace StardewModdingAPI
this.IsDisposed = true; this.IsDisposed = true;
// dispose mod data // dispose mod data
foreach (IModMetadata mod in this.ModRegistry.GetMods()) foreach (IModMetadata mod in this.ModRegistry.GetAll())
{ {
try try
{ {
@ -374,7 +374,7 @@ namespace StardewModdingAPI
} }
// update window titles // 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"; 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"; 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(); LocalizedContentManager.LanguageCode languageCode = this.ContentManager.GetCurrentLanguage();
// update mod translation helpers // 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); (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); AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.TargetPlatform, this.Monitor, this.Settings.DeveloperMode);
AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => modAssemblyLoader.ResolveAssembly(e.Name); AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => modAssemblyLoader.ResolveAssembly(e.Name);
InterfaceProxyBuilder proxyBuilder = new InterfaceProxyBuilder();
foreach (IModMetadata metadata in mods) foreach (IModMetadata metadata in mods)
{ {
// get basic info // get basic info
@ -696,53 +697,30 @@ namespace StardewModdingAPI
continue; 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 // initialise mod
try try
{ {
// get implementation // init mod helpers
TypeInfo modEntryType = modAssembly.DefinedTypes.First(type => typeof(Mod).IsAssignableFrom(type) && !type.IsAbstract);
Mod mod = (Mod)modAssembly.CreateInstance(modEntryType.ToString());
if (mod == null)
{
TrackSkip(metadata, "its entry class couldn't be instantiated.");
continue;
}
// inject data
{
IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName); IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName);
IModHelper modHelper;
{
ICommandHelper commandHelper = new CommandHelper(manifest.UniqueID, metadata.DisplayName, this.CommandManager); ICommandHelper commandHelper = new CommandHelper(manifest.UniqueID, metadata.DisplayName, this.CommandManager);
IContentHelper contentHelper = new ContentHelper(contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); IContentHelper contentHelper = new ContentHelper(contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor);
IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, metadata.DisplayName, this.Reflection, this.DeprecationManager); 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()); ITranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentManager.GetLocale(), contentManager.GetCurrentLanguage());
modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, translationHelper);
mod.ModManifest = manifest;
mod.Helper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, translationHelper);
mod.Monitor = monitor;
} }
// 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 // track mod
metadata.SetMod(mod); metadata.SetMod(mod);
this.ModRegistry.Add(metadata); 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 // log skipped mods
this.Monitor.Newline(); 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); 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 // invalidate cache entries when needed
@ -852,13 +843,48 @@ namespace StardewModdingAPI
this.Monitor.Log("Invalidating cached assets for new editors & loaders...", LogLevel.Trace); this.Monitor.Log("Invalidating cached assets for new editors & loaders...", LogLevel.Trace);
this.ContentManager.InvalidateCacheFor(editors, loaders); 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> /// <summary>Reload translations for all mods.</summary>
private void ReloadTranslations() private void ReloadTranslations()
{ {
JsonHelper jsonHelper = new JsonHelper(); JsonHelper jsonHelper = new JsonHelper();
foreach (IModMetadata metadata in this.ModRegistry.GetMods()) foreach (IModMetadata metadata in this.ModRegistry.GetAll())
{ {
// read translation files // read translation files
IDictionary<string, IDictionary<string, string>> translations = new Dictionary<string, IDictionary<string, string>>(); IDictionary<string, IDictionary<string, string>> translations = new Dictionary<string, IDictionary<string, string>>();

View File

@ -79,10 +79,6 @@
<Private>True</Private> <Private>True</Private>
</Reference> </Reference>
<Reference Include="System.Windows.Forms" Condition="$(OS) == 'Windows_NT'" /> <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" /> <Reference Include="System.Xml" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -110,6 +106,7 @@
<Compile Include="Framework\ContentManagerShim.cs" /> <Compile Include="Framework\ContentManagerShim.cs" />
<Compile Include="Framework\Exceptions\SAssemblyLoadFailedException.cs" /> <Compile Include="Framework\Exceptions\SAssemblyLoadFailedException.cs" />
<Compile Include="Framework\ModLoading\AssemblyLoadStatus.cs" /> <Compile Include="Framework\ModLoading\AssemblyLoadStatus.cs" />
<Compile Include="Framework\Reflection\InterfaceProxyBuilder.cs" />
<Compile Include="Framework\Utilities\ContextHash.cs" /> <Compile Include="Framework\Utilities\ContextHash.cs" />
<Compile Include="IReflectedField.cs" /> <Compile Include="IReflectedField.cs" />
<Compile Include="IReflectedMethod.cs" /> <Compile Include="IReflectedMethod.cs" />