From 2c909f26fcf48fc1de7f3b23f5f83d28d4a5e253 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 11 Dec 2017 23:33:10 -0500 Subject: [PATCH] add prototype of mod-provided APIs (#409) --- src/SMAPI/Framework/IModMetadata.cs | 6 +- .../Framework/ModHelpers/ModRegistryHelper.cs | 6 + src/SMAPI/Framework/ModLoading/ModMetadata.cs | 7 +- src/SMAPI/IModProvidedApi.cs | 6 + src/SMAPI/IModRegistry.cs | 8 +- src/SMAPI/Program.cs | 152 +++++++++++++----- src/SMAPI/StardewModdingAPI.csproj | 1 + 7 files changed, 145 insertions(+), 41 deletions(-) create mode 100644 src/SMAPI/IModProvidedApi.cs 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 /// The mod instance (if it was loaded). IMod Mod { get; } + /// The mod-provided API (if any). + IModProvidedApi Api { get; } + /********* ** Public methods @@ -42,6 +45,7 @@ namespace StardewModdingAPI.Framework /// Set the mod instance. /// The mod instance to set. - IModMetadata SetMod(IMod mod); + /// The mod-provided API (if any). + 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; } + + /// 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 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 /// The mod instance (if it was loaded). public IMod Mod { get; private set; } + /// The mod-provided API (if any). + public IModProvidedApi Api { get; private set; } + /********* ** Public methods @@ -59,9 +62,11 @@ namespace StardewModdingAPI.Framework.ModLoading /// Set the mod instance. /// The mod instance to set. - public IModMetadata SetMod(IMod mod) + /// The mod-provided API (if any). + 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 +{ + /// An API provided by a mod for other mods to use. + /// This is a marker interface. Each mod can only have one implementation of . + 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 /// 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. + 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 } } + /// 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; + } + + /// Load a mod's implementation. + /// The mod assembly. + /// The mod's helper instance. + /// The mod's monitor instance. + /// A callback invoked when loading fails. + /// The loaded instance. + private bool TryLoadModProvidedApi(Assembly modAssembly, IModHelper modHelper, IMonitor monitor, Action 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; + } + } + /// Reload translations for all mods. 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 @@ +