diff options
author | Jesse Plamondon-Willard <github@jplamondonw.com> | 2017-12-11 23:33:10 -0500 |
---|---|---|
committer | Jesse Plamondon-Willard <github@jplamondonw.com> | 2017-12-11 23:33:10 -0500 |
commit | 2c909f26fcf48fc1de7f3b23f5f83d28d4a5e253 (patch) | |
tree | 437e1eedf1bddf7204a800104c370cf238dcac3b | |
parent | 971aff8368a8a2c196d942984926efc2f80cc216 (diff) | |
download | SMAPI-2c909f26fcf48fc1de7f3b23f5f83d28d4a5e253.tar.gz SMAPI-2c909f26fcf48fc1de7f3b23f5f83d28d4a5e253.tar.bz2 SMAPI-2c909f26fcf48fc1de7f3b23f5f83d28d4a5e253.zip |
add prototype of mod-provided APIs (#409)
-rw-r--r-- | src/SMAPI/Framework/IModMetadata.cs | 6 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs | 6 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModLoading/ModMetadata.cs | 7 | ||||
-rw-r--r-- | src/SMAPI/IModProvidedApi.cs | 6 | ||||
-rw-r--r-- | src/SMAPI/IModRegistry.cs | 8 | ||||
-rw-r--r-- | src/SMAPI/Program.cs | 152 | ||||
-rw-r--r-- | src/SMAPI/StardewModdingAPI.csproj | 1 |
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" /> |