summaryrefslogtreecommitdiff
path: root/src/SMAPI
diff options
context:
space:
mode:
authorJesse Plamondon-Willard <github@jplamondonw.com>2017-12-11 23:33:10 -0500
committerJesse Plamondon-Willard <github@jplamondonw.com>2017-12-11 23:33:10 -0500
commit2c909f26fcf48fc1de7f3b23f5f83d28d4a5e253 (patch)
tree437e1eedf1bddf7204a800104c370cf238dcac3b /src/SMAPI
parent971aff8368a8a2c196d942984926efc2f80cc216 (diff)
downloadSMAPI-2c909f26fcf48fc1de7f3b23f5f83d28d4a5e253.tar.gz
SMAPI-2c909f26fcf48fc1de7f3b23f5f83d28d4a5e253.tar.bz2
SMAPI-2c909f26fcf48fc1de7f3b23f5f83d28d4a5e253.zip
add prototype of mod-provided APIs (#409)
Diffstat (limited to 'src/SMAPI')
-rw-r--r--src/SMAPI/Framework/IModMetadata.cs6
-rw-r--r--src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs6
-rw-r--r--src/SMAPI/Framework/ModLoading/ModMetadata.cs7
-rw-r--r--src/SMAPI/IModProvidedApi.cs6
-rw-r--r--src/SMAPI/IModRegistry.cs8
-rw-r--r--src/SMAPI/Program.cs152
-rw-r--r--src/SMAPI/StardewModdingAPI.csproj1
7 files changed, 145 insertions, 41 deletions
diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs
index c21734a7..c4be7daf 100644
--- a/src/SMAPI/Framework/IModMetadata.cs
+++ b/src/SMAPI/Framework/IModMetadata.cs
@@ -30,6 +30,9 @@ namespace StardewModdingAPI.Framework
/// <summary>The mod instance (if it was loaded).</summary>
IMod Mod { get; }
+ /// <summary>The mod-provided API (if any).</summary>
+ IModProvidedApi Api { get; }
+
/*********
** Public methods
@@ -42,6 +45,7 @@ namespace StardewModdingAPI.Framework
/// <summary>Set the mod instance.</summary>
/// <param name="mod">The mod instance to set.</param>
- IModMetadata SetMod(IMod mod);
+ /// <param name="api">The mod-provided API (if any).</param>
+ IModMetadata SetMod(IMod mod, IModProvidedApi api);
}
}
diff --git a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs
index 4e3f56de..340205f3 100644
--- a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs
@@ -45,5 +45,11 @@ namespace StardewModdingAPI.Framework.ModHelpers
{
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 IModProvidedApi GetApi(string uniqueID)
+ {
+ return this.Registry.Get(uniqueID)?.Api;
+ }
}
}
diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs
index 5055da75..2e5c27be 100644
--- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs
+++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs
@@ -29,6 +29,9 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <summary>The mod instance (if it was loaded).</summary>
public IMod Mod { get; private set; }
+ /// <summary>The mod-provided API (if any).</summary>
+ public IModProvidedApi Api { get; private set; }
+
/*********
** Public methods
@@ -59,9 +62,11 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <summary>Set the mod instance.</summary>
/// <param name="mod">The mod instance to set.</param>
- public IModMetadata SetMod(IMod mod)
+ /// <param name="api">The mod-provided API (if any).</param>
+ public IModMetadata SetMod(IMod mod, IModProvidedApi api)
{
this.Mod = mod;
+ this.Api = api;
return this;
}
}
diff --git a/src/SMAPI/IModProvidedApi.cs b/src/SMAPI/IModProvidedApi.cs
new file mode 100644
index 00000000..9884ca78
--- /dev/null
+++ b/src/SMAPI/IModProvidedApi.cs
@@ -0,0 +1,6 @@
+namespace StardewModdingAPI
+{
+ /// <summary>An API provided by a mod for other mods to use.</summary>
+ /// <remarks>This is a marker interface. Each mod can only have one implementation of <see cref="IModProvidedApi"/>.</remarks>
+ public interface IModProvidedApi { }
+}
diff --git a/src/SMAPI/IModRegistry.cs b/src/SMAPI/IModRegistry.cs
index 5ef3fd65..fd71d72a 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,9 @@ 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>
+ IModProvidedApi GetApi(string uniqueID);
}
-} \ No newline at end of file
+}
diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs
index bd4692e6..6330cc1a 100644
--- a/src/SMAPI/Program.cs
+++ b/src/SMAPI/Program.cs
@@ -696,55 +696,34 @@ 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);
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 instances
+ if (!this.TryLoadModEntry(modAssembly, error => TrackSkip(metadata, error), out Mod mod))
+ continue;
+ if (this.TryLoadModProvidedApi(modAssembly, modHelper, monitor, error => this.Monitor.Log($"Failed loading {metadata.DisplayName}'s mod-provided API. Integrations may not work correctly. Error: {error}", LogLevel.Warn), out IModProvidedApi api))
+ this.Monitor.Log($" Found mod-provided API ({api.GetType().FullName}).", LogLevel.Trace);
+
+ // init mod
+ mod.ModManifest = manifest;
+ mod.Helper = modHelper;
+ mod.Monitor = monitor;
+
// track mod
- metadata.SetMod(mod);
+ metadata.SetMod(mod, api);
this.ModRegistry.Add(metadata);
}
catch (Exception ex)
@@ -854,6 +833,105 @@ namespace StardewModdingAPI
}
}
+ /// <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>Load a mod's <see cref="IModProvidedApi"/> implementation.</summary>
+ /// <param name="modAssembly">The mod assembly.</param>
+ /// <param name="modHelper">The mod's helper instance.</param>
+ /// <param name="monitor">The mod's monitor instance.</param>
+ /// <param name="onError">A callback invoked when loading fails.</param>
+ /// <param name="api">The loaded instance.</param>
+ private bool TryLoadModProvidedApi(Assembly modAssembly, IModHelper modHelper, IMonitor monitor, Action<string> onError, out IModProvidedApi api)
+ {
+ api = null;
+
+ // find type
+ TypeInfo[] apis = modAssembly.DefinedTypes.Where(type => typeof(IModProvidedApi).IsAssignableFrom(type) && !type.IsAbstract).Take(2).ToArray();
+ if (apis.Length == 0)
+ return false;
+ if (apis.Length > 1)
+ {
+ onError($"its DLL contains multiple '{nameof(IModProvidedApi)}' implementations.");
+ return false;
+ }
+
+ // get constructor
+ ConstructorInfo constructor = (
+ from constr in apis[0].GetConstructors()
+ let args = constr.GetParameters()
+ where
+ !args.Any()
+ || args.All(arg => typeof(IModHelper).IsAssignableFrom(arg.ParameterType) || typeof(IMonitor).IsAssignableFrom(arg.ParameterType))
+ orderby args.Length descending
+ select constr
+ ).FirstOrDefault();
+ if (constructor == null)
+ {
+ onError($"its {nameof(IModProvidedApi)} must have a constructor with zero arguments, or only arguments of type {nameof(IModHelper)} or {nameof(IMonitor)}.");
+ return false;
+ }
+
+ // construct instance
+ try
+ {
+ // prepare constructor args
+ ParameterInfo[] args = constructor.GetParameters();
+ object[] values = new object[args.Length];
+ for (int i = 0; i < args.Length; i++)
+ {
+ if (typeof(IModHelper).IsAssignableFrom(args[i].ParameterType))
+ values[i] = modHelper;
+ else if (typeof(IMonitor).IsAssignableFrom(args[i].ParameterType))
+ values[i] = monitor;
+ else
+ {
+ // shouldn't happen
+ onError($"its {nameof(IModProvidedApi)} instance's constructor has unexpected argument type {args[i].ParameterType.FullName}.");
+ return false;
+ }
+ }
+
+ // instantiate
+ api = (IModProvidedApi)constructor.Invoke(values);
+ return true;
+ }
+ catch (Exception ex)
+ {
+ onError($"its {nameof(IModProvidedApi)} couldn't be constructed: {ex.GetLogSummary()}");
+ return false;
+ }
+ }
+
/// <summary>Reload translations for all mods.</summary>
private void ReloadTranslations()
{
diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj
index 0db94843..579ed487 100644
--- a/src/SMAPI/StardewModdingAPI.csproj
+++ b/src/SMAPI/StardewModdingAPI.csproj
@@ -114,6 +114,7 @@
<Compile Include="IReflectedField.cs" />
<Compile Include="IReflectedMethod.cs" />
<Compile Include="IReflectedProperty.cs" />
+ <Compile Include="IModProvidedApi.cs" />
<Compile Include="Metadata\CoreAssets.cs" />
<Compile Include="ContentSource.cs" />
<Compile Include="Events\ContentEvents.cs" />