diff --git a/docs/release-notes.md b/docs/release-notes.md
index 0e2477f4..8407455c 100644
--- a/docs/release-notes.md
+++ b/docs/release-notes.md
@@ -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.
diff --git a/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj b/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj
index f228bb25..a65ad72c 100644
--- a/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj
+++ b/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj
@@ -41,10 +41,6 @@
False
-
-
-
-
diff --git a/src/SMAPI/Events/GameEvents.cs b/src/SMAPI/Events/GameEvents.cs
index b477376e..3466470d 100644
--- a/src/SMAPI/Events/GameEvents.cs
+++ b/src/SMAPI/Events/GameEvents.cs
@@ -33,6 +33,9 @@ namespace StardewModdingAPI.Events
/// Raised every 60th tick (≈once per second).
public static event EventHandler OneSecondTick;
+ /// Raised once after the game initialises and all methods have been called.
+ public static event EventHandler FirstUpdateTick;
+
/*********
** Internal methods
@@ -92,5 +95,12 @@ namespace StardewModdingAPI.Events
{
monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.OneSecondTick)}", GameEvents.OneSecondTick?.GetInvocationList());
}
+
+ /// Raise a event.
+ /// Encapsulates monitoring and logging.
+ internal static void InvokeFirstUpdateTick(IMonitor monitor)
+ {
+ monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.FirstUpdateTick)}", GameEvents.FirstUpdateTick?.GetInvocationList());
+ }
}
}
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
/// How deprecated the code is.
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);
}
/// Log a deprecation warning.
@@ -82,7 +82,7 @@ namespace StardewModdingAPI.Framework
/// Returns whether the deprecation was successfully marked as warned. Returns false if it was already marked.
public bool MarkWarned(string nounPhrase, string version)
{
- return this.MarkWarned(this.ModRegistry.GetModFromStack(), nounPhrase, version);
+ return this.MarkWarned(this.ModRegistry.GetFromStack()?.DisplayName, nounPhrase, version);
}
/// Mark a deprecation warning as already logged.
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
/// The mod instance (if it was loaded).
IMod Mod { get; }
+ /// The mod-provided API (if any).
+ object Api { get; }
+
/*********
** Public methods
@@ -43,5 +46,9 @@ namespace StardewModdingAPI.Framework
/// Set the mod instance.
/// The mod instance to set.
IModMetadata SetMod(IMod mod);
+
+ /// Set the mod-provided API instance.
+ /// The mod-provided API.
+ 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
/// The underlying mod registry.
private readonly ModRegistry Registry;
+ /// Encapsulates monitoring and logging for the mod.
+ private readonly IMonitor Monitor;
+
+ /// The mod IDs for APIs accessed by this instanced.
+ private readonly HashSet AccessedModApis = new HashSet();
+
+ /// Generates proxy classes to access mod APIs through an arbitrary interface.
+ private readonly InterfaceProxyBuilder ProxyBuilder;
+
/*********
** Public methods
@@ -18,16 +29,20 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// Construct an instance.
/// The unique ID of the relevant mod.
/// The underlying mod registry.
- public ModRegistryHelper(string modID, ModRegistry registry)
+ /// Generates proxy classes to access mod APIs through an arbitrary interface.
+ /// Encapsulates monitoring and logging for the mod.
+ public ModRegistryHelper(string modID, ModRegistry registry, InterfaceProxyBuilder proxyBuilder, IMonitor monitor)
: base(modID)
{
this.Registry = registry;
+ this.ProxyBuilder = proxyBuilder;
+ this.Monitor = monitor;
}
/// Get metadata for all loaded mods.
public IEnumerable GetAll()
{
- return this.Registry.GetAll();
+ return this.Registry.GetAll().Select(p => p.Manifest);
}
/// Get metadata for a loaded mod.
@@ -35,14 +50,56 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// Returns the matching mod's metadata, or null if not found.
public IManifest Get(string uniqueID)
{
- return this.Registry.Get(uniqueID);
+ return this.Registry.Get(uniqueID)?.Manifest;
}
/// Get whether a mod has been loaded.
/// The mod's unique ID.
public bool IsLoaded(string uniqueID)
{
- return this.Registry.IsLoaded(uniqueID);
+ return this.Registry.Get(uniqueID) != null;
+ }
+
+ /// Get the API provided by a mod, or null if it has none. This signature requires using the API to access the API's properties and methods.
+ 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;
+ }
+
+ /// 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 null.
+ /// The interface which matches the properties and methods you intend to access.
+ /// The mod's unique ID.
+ public TInterface GetApi(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(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
/// The mod instance (if it was loaded).
public IMod Mod { get; private set; }
+ /// The mod-provided API (if any).
+ public object Api { get; private set; }
+
/*********
** Public methods
@@ -64,5 +67,13 @@ namespace StardewModdingAPI.Framework.ModLoading
this.Mod = mod;
return this;
}
+
+ /// Set the mod-provided API instance.
+ /// The mod-provided API.
+ 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
/// The registered mod data.
private readonly List Mods = new List();
- /// The friendly mod names treated as deprecation warning sources (assembly full name => mod name).
- private readonly IDictionary ModNamesByAssembly = new Dictionary();
+ /// An assembly full name => mod lookup.
+ private readonly IDictionary ModNamesByAssembly = new Dictionary();
+
+ /// Whether all mods have been initialised and their method called.
+ public bool AreAllModsInitialised { get; set; }
/*********
** Public methods
*********/
- /****
- ** Basic metadata
- ****/
- /// Get metadata for all loaded mods.
- public IEnumerable GetAll()
+ /// Register a mod as a possible source of deprecation warnings.
+ /// The mod metadata.
+ public void Add(IModMetadata metadata)
{
- return this.Mods.Select(p => p.Manifest);
+ this.Mods.Add(metadata);
+ this.ModNamesByAssembly[metadata.Mod.GetType().Assembly.FullName] = metadata;
+ }
+
+ /// Get metadata for all loaded mods.
+ public IEnumerable GetAll()
+ {
+ return this.Mods.Select(p => p);
}
/// Get metadata for a loaded mod.
/// The mod's unique ID.
/// Returns the matching mod's metadata, or null if not found.
- 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));
}
- /// Get whether a mod has been loaded.
- /// The mod's unique ID.
- public bool IsLoaded(string uniqueID)
- {
- return this.Get(uniqueID) != null;
- }
-
- /****
- ** Mod data
- ****/
- /// Register a mod as a possible source of deprecation warnings.
- /// The mod metadata.
- public void Add(IModMetadata metadata)
- {
- this.Mods.Add(metadata);
- this.ModNamesByAssembly[metadata.Mod.GetType().Assembly.FullName] = metadata.DisplayName;
- }
-
- /// Get all enabled mods.
- public IEnumerable GetMods()
- {
- return (from mod in this.Mods select mod);
- }
-
- /// Get the friendly mod name which defines a type.
+ /// Get the mod metadata from one of its assemblies.
/// The type to check.
/// Returns the mod name, or null if the type isn't part of a known mod.
- public string GetModFrom(Type type)
+ public IModMetadata GetFrom(Type type)
{
// null
if (type == null)
@@ -89,7 +73,7 @@ namespace StardewModdingAPI.Framework
/// Get the friendly name for the closest assembly registered as a source of deprecation warnings.
/// Returns the source name, or null if no registered assemblies were found.
- 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
+{
+ /// Generates proxy classes to access mod APIs through an arbitrary interface.
+ internal class InterfaceProxyBuilder
+ {
+ /*********
+ ** Properties
+ *********/
+ /// The CLR module in which to create proxy classes.
+ private readonly ModuleBuilder ModuleBuilder;
+
+ /// The generated proxy types.
+ private readonly IDictionary GeneratedTypes = new Dictionary();
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ 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");
+ }
+
+ /// Create an API proxy.
+ /// The interface through which to access the API.
+ /// The API instance to access.
+ /// The unique ID of the mod consuming the API.
+ /// The unique ID of the mod providing the API.
+ public TInterface CreateProxy(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
+ *********/
+ /// Define a class which proxies access to a target type through an interface.
+ /// The name of the proxy type to generate.
+ /// The interface type through which to access the target.
+ /// The target type to access.
+ 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();
+ }
+
+ /// Define a method which proxies access to a method on the target.
+ /// The proxy type being generated.
+ /// The target method.
+ /// The proxy field containing the API instance.
+ 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();
}
diff --git a/src/SMAPI/IMod.cs b/src/SMAPI/IMod.cs
index 35ac7c0f..44ef32c9 100644
--- a/src/SMAPI/IMod.cs
+++ b/src/SMAPI/IMod.cs
@@ -1,4 +1,4 @@
-namespace StardewModdingAPI
+namespace StardewModdingAPI
{
/// The implementation for a Stardew Valley mod.
public interface IMod
@@ -22,5 +22,8 @@
/// The mod entry point, called after the mod is first loaded.
/// Provides simplified APIs for writing mods.
void Entry(IModHelper helper);
+
+ /// Get an API that other mods can access. This is always called after .
+ object GetApi();
}
-}
\ No newline at end of file
+}
diff --git a/src/SMAPI/IModRegistry.cs b/src/SMAPI/IModRegistry.cs
index 5ef3fd65..a06e099e 100644
--- a/src/SMAPI/IModRegistry.cs
+++ b/src/SMAPI/IModRegistry.cs
@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
namespace StardewModdingAPI
{
@@ -16,5 +16,14 @@ namespace StardewModdingAPI
/// Get whether a mod has been loaded.
/// The mod's unique ID.
bool IsLoaded(string uniqueID);
+
+ /// Get the API provided by a mod, or null if it has none. This signature requires using the API to access the API's properties and methods.
+ /// The mod's unique ID.
+ object GetApi(string uniqueID);
+
+ /// 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 null.
+ /// The interface which matches the properties and methods you intend to access.
+ /// The mod's unique ID.
+ TInterface GetApi(string uniqueID) where TInterface : class;
}
-}
\ No newline at end of file
+}
diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs
index 3346f1ac..f285764c 100644
--- a/src/SMAPI/Metadata/InstructionMetadata.cs
+++ b/src/SMAPI/Metadata/InstructionMetadata.cs
@@ -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),
diff --git a/src/SMAPI/Mod.cs b/src/SMAPI/Mod.cs
index ee75ba54..3a753afc 100644
--- a/src/SMAPI/Mod.cs
+++ b/src/SMAPI/Mod.cs
@@ -25,6 +25,9 @@ namespace StardewModdingAPI
/// Provides simplified APIs for writing mods.
public abstract void Entry(IModHelper helper);
+ /// Get an API that other mods can access. This is always called after .
+ public virtual object GetApi() => null;
+
/// Release or reset unmanaged resources.
public void Dispose()
{
diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs
index 8bc2c675..7eda9c66 100644
--- a/src/SMAPI/Program.cs
+++ b/src/SMAPI/Program.cs
@@ -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;
+ }
+
+ /// Load a mod's entry class.
+ /// The mod assembly.
+ /// A callback invoked when loading fails.
+ /// The loaded instance.
+ private bool TryLoadModEntry(Assembly modAssembly, Action 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;
}
/// Reload translations for all mods.
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> translations = new Dictionary>();
diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj
index 0db94843..f76ac439 100644
--- a/src/SMAPI/StardewModdingAPI.csproj
+++ b/src/SMAPI/StardewModdingAPI.csproj
@@ -79,10 +79,6 @@
True
-
-
-
-
@@ -110,6 +106,7 @@
+