From 971aff8368a8a2c196d942984926efc2f80cc216 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 11 Dec 2017 22:29:56 -0500 Subject: [PATCH 01/12] generalise internal mod registry (#409) --- src/SMAPI/Framework/DeprecationManager.cs | 4 +- .../Framework/ModHelpers/ModRegistryHelper.cs | 9 +-- src/SMAPI/Framework/ModRegistry.cs | 61 +++++++------------ src/SMAPI/Program.cs | 10 +-- 4 files changed, 33 insertions(+), 51 deletions(-) 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/ModHelpers/ModRegistryHelper.cs b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs index 9e824694..4e3f56de 100644 --- a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System.Collections.Generic; +using System.Linq; namespace StardewModdingAPI.Framework.ModHelpers { @@ -27,7 +28,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// 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 +36,14 @@ 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; } } } diff --git a/src/SMAPI/Framework/ModRegistry.cs b/src/SMAPI/Framework/ModRegistry.cs index 9dde7a20..4dbc3541 100644 --- a/src/SMAPI/Framework/ModRegistry.cs +++ b/src/SMAPI/Framework/ModRegistry.cs @@ -15,26 +15,31 @@ 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(); /********* ** 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 +47,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 +70,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 +82,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/Program.cs b/src/SMAPI/Program.cs index 8bc2c675..bd4692e6 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); } @@ -753,7 +753,7 @@ namespace StardewModdingAPI } } } - IModMetadata[] loadedMods = this.ModRegistry.GetMods().ToArray(); + IModMetadata[] loadedMods = this.ModRegistry.GetAll().ToArray(); // log skipped mods this.Monitor.Newline(); @@ -858,7 +858,7 @@ namespace StardewModdingAPI 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>(); From 2c909f26fcf48fc1de7f3b23f5f83d28d4a5e253 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 11 Dec 2017 23:33:10 -0500 Subject: [PATCH 02/12] 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 @@ + From 7d644aeabee63c0d51d4e89360d2fdab0e51b8be Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 12 Dec 2017 00:09:28 -0500 Subject: [PATCH 03/12] switch to simpler approach for mod-provided APIs (#409) --- src/SMAPI/Framework/IModMetadata.cs | 9 +- .../Framework/ModHelpers/ModRegistryHelper.cs | 2 +- src/SMAPI/Framework/ModLoading/ModMetadata.cs | 12 ++- src/SMAPI/IMod.cs | 7 +- src/SMAPI/IModProvidedApi.cs | 6 -- src/SMAPI/IModRegistry.cs | 2 +- src/SMAPI/Mod.cs | 3 + src/SMAPI/Program.cs | 86 ++++--------------- src/SMAPI/StardewModdingAPI.csproj | 1 - 9 files changed, 40 insertions(+), 88 deletions(-) delete mode 100644 src/SMAPI/IModProvidedApi.cs diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs index c4be7daf..a36994fd 100644 --- a/src/SMAPI/Framework/IModMetadata.cs +++ b/src/SMAPI/Framework/IModMetadata.cs @@ -31,7 +31,7 @@ namespace StardewModdingAPI.Framework IMod Mod { get; } /// The mod-provided API (if any). - IModProvidedApi Api { get; } + object Api { get; } /********* @@ -45,7 +45,10 @@ namespace StardewModdingAPI.Framework /// Set the mod instance. /// The mod instance to set. - /// The mod-provided API (if any). - IModMetadata SetMod(IMod mod, IModProvidedApi api); + 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 340205f3..949d986a 100644 --- a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs @@ -47,7 +47,7 @@ namespace StardewModdingAPI.Framework.ModHelpers } /// 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) + public object 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 2e5c27be..30fe211b 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -30,7 +30,7 @@ namespace StardewModdingAPI.Framework.ModLoading public IMod Mod { get; private set; } /// The mod-provided API (if any). - public IModProvidedApi Api { get; private set; } + public object Api { get; private set; } /********* @@ -62,10 +62,16 @@ namespace StardewModdingAPI.Framework.ModLoading /// Set the mod instance. /// The mod instance to set. - /// The mod-provided API (if any). - public IModMetadata SetMod(IMod mod, IModProvidedApi api) + public IModMetadata SetMod(IMod mod) { 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/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/IModProvidedApi.cs b/src/SMAPI/IModProvidedApi.cs deleted file mode 100644 index 9884ca78..00000000 --- a/src/SMAPI/IModProvidedApi.cs +++ /dev/null @@ -1,6 +0,0 @@ -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 fd71d72a..f84cfcfb 100644 --- a/src/SMAPI/IModRegistry.cs +++ b/src/SMAPI/IModRegistry.cs @@ -19,6 +19,6 @@ namespace StardewModdingAPI /// 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); + object GetApi(string uniqueID); } } 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 6330cc1a..e7552630 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -711,11 +711,9 @@ namespace StardewModdingAPI modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, translationHelper); } - // get mod instances + // get mod instance 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; @@ -723,7 +721,7 @@ namespace StardewModdingAPI mod.Monitor = monitor; // track mod - metadata.SetMod(mod, api); + metadata.SetMod(mod); this.ModRegistry.Add(metadata); } catch (Exception ex) @@ -796,6 +794,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 @@ -865,73 +876,6 @@ namespace StardewModdingAPI 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 579ed487..0db94843 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -114,7 +114,6 @@ - From d04cacbdd0729140e4d8e93323ba66ee90ff9d2a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 12 Dec 2017 00:16:34 -0500 Subject: [PATCH 04/12] log mod-provided API access (#409) --- .../Framework/ModHelpers/ModRegistryHelper.cs | 15 +++++++++++++-- src/SMAPI/Program.cs | 2 +- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs index 949d986a..827c77d5 100644 --- a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs @@ -12,6 +12,12 @@ 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(); + /********* ** Public methods @@ -19,10 +25,12 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Construct an instance. /// The unique ID of the relevant mod. /// The underlying mod registry. - public ModRegistryHelper(string modID, ModRegistry registry) + /// Encapsulates monitoring and logging for the mod. + public ModRegistryHelper(string modID, ModRegistry registry, IMonitor monitor) : base(modID) { this.Registry = registry; + this.Monitor = monitor; } /// Get metadata for all loaded mods. @@ -49,7 +57,10 @@ namespace StardewModdingAPI.Framework.ModHelpers /// 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) { - return this.Registry.Get(uniqueID)?.Api; + 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; } } } diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index e7552630..17fe2f36 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -706,7 +706,7 @@ namespace StardewModdingAPI 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, monitor); 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); } From 0e43041777d68b96f110fa38ad7424b855db761a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 12 Dec 2017 01:00:32 -0500 Subject: [PATCH 05/12] add support for casting mod-provided API to an interface without a direct assembly reference (#409) --- build/common.targets | 1 + build/prepare-install-package.targets | 2 ++ .../Framework/ModHelpers/ModRegistryHelper.cs | 24 +++++++++++++++++++ src/SMAPI/IModRegistry.cs | 5 ++++ src/SMAPI/StardewModdingAPI.csproj | 3 +++ src/SMAPI/packages.config | 1 + 6 files changed, 36 insertions(+) diff --git a/build/common.targets b/build/common.targets index aa11344e..15c935e3 100644 --- a/build/common.targets +++ b/build/common.targets @@ -86,6 +86,7 @@ + diff --git a/build/prepare-install-package.targets b/build/prepare-install-package.targets index f2a2b23c..e88ca273 100644 --- a/build/prepare-install-package.targets +++ b/build/prepare-install-package.targets @@ -22,6 +22,7 @@ + @@ -36,6 +37,7 @@ + diff --git a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs index 827c77d5..68201d9a 100644 --- a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using ImpromptuInterface; namespace StardewModdingAPI.Framework.ModHelpers { @@ -62,5 +63,28 @@ namespace StardewModdingAPI.Framework.ModHelpers 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 (!typeof(TInterface).IsInterface) + { + this.Monitor.Log("Tried to map a mod-provided API into a class; must be an interface."); + 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 api.ActLike(); + } } } diff --git a/src/SMAPI/IModRegistry.cs b/src/SMAPI/IModRegistry.cs index f84cfcfb..a06e099e 100644 --- a/src/SMAPI/IModRegistry.cs +++ b/src/SMAPI/IModRegistry.cs @@ -20,5 +20,10 @@ namespace StardewModdingAPI /// 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; } } diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 0db94843..c585a0c4 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -53,6 +53,9 @@ icon.ico + + ..\packages\ImpromptuInterface.6.2.2\lib\net40\ImpromptuInterface.dll + ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.dll True diff --git a/src/SMAPI/packages.config b/src/SMAPI/packages.config index 98d742c7..60be6881 100644 --- a/src/SMAPI/packages.config +++ b/src/SMAPI/packages.config @@ -1,5 +1,6 @@  + \ No newline at end of file From 59a25a12ffdb90c5a9a3db90be933ca4e76eb64f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 12 Dec 2017 01:09:43 -0500 Subject: [PATCH 06/12] validate interface is public (#409) --- src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs index 68201d9a..d39c885c 100644 --- a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs @@ -72,7 +72,12 @@ namespace StardewModdingAPI.Framework.ModHelpers // validate if (!typeof(TInterface).IsInterface) { - this.Monitor.Log("Tried to map a mod-provided API into a class; must be an interface."); + 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; } From e00424068f3da7c4f91187872e96c90fa61e47db Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 12 Dec 2017 01:33:11 -0500 Subject: [PATCH 07/12] block access to mod-provided APIs until all mods are initialised (#409) --- src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs | 5 +++++ src/SMAPI/Framework/ModRegistry.cs | 3 +++ src/SMAPI/Program.cs | 3 +++ 3 files changed, 11 insertions(+) diff --git a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs index d39c885c..9574a632 100644 --- a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs @@ -70,6 +70,11 @@ namespace StardewModdingAPI.Framework.ModHelpers 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); diff --git a/src/SMAPI/Framework/ModRegistry.cs b/src/SMAPI/Framework/ModRegistry.cs index 4dbc3541..453d2868 100644 --- a/src/SMAPI/Framework/ModRegistry.cs +++ b/src/SMAPI/Framework/ModRegistry.cs @@ -18,6 +18,9 @@ namespace StardewModdingAPI.Framework /// 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 diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 17fe2f36..786549fe 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -842,6 +842,9 @@ 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. From ef23043e1f63c4c910cc59497d6244e3727c92f9 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 12 Dec 2017 01:56:32 -0500 Subject: [PATCH 08/12] reintroduce GameEvents.FirstUpdateTick to simplify mod integrations (#409) --- src/SMAPI/Events/GameEvents.cs | 10 ++++++++++ src/SMAPI/Framework/SGame.cs | 13 ++++++++----- src/SMAPI/Metadata/InstructionMetadata.cs | 1 - 3 files changed, 18 insertions(+), 6 deletions(-) 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/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/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), From a391dfe26372714a395abc5ee77b603e29562691 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 12 Dec 2017 01:58:42 -0500 Subject: [PATCH 09/12] update release notes (#409) --- docs/release-notes.md | 2 ++ 1 file changed, 2 insertions(+) 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. From 356503325f4ef5ed84c359d4b56991c2a7f18b8e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 12 Dec 2017 18:44:38 -0500 Subject: [PATCH 10/12] fix compatibility on Mac (#409) --- build/prepare-install-package.targets | 1 + src/SMAPI/StardewModdingAPI.csproj | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/build/prepare-install-package.targets b/build/prepare-install-package.targets index e88ca273..f8262271 100644 --- a/build/prepare-install-package.targets +++ b/build/prepare-install-package.targets @@ -24,6 +24,7 @@ + diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index c585a0c4..026ac106 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -84,7 +84,10 @@ - + + + True + From 21fd2d1e39a6a94758f6298c2da52cd46cffdfcd Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 15 Dec 2017 21:37:08 -0500 Subject: [PATCH 11/12] emit proxy classes directly to simplify crossplatform compatibility (#409) --- build/common.targets | 1 - build/prepare-install-package.targets | 2 - .../Framework/ModHelpers/ModRegistryHelper.cs | 11 +- .../Reflection/InterfaceProxyBuilder.cs | 138 ++++++++++++++++++ src/SMAPI/Program.cs | 3 +- src/SMAPI/StardewModdingAPI.csproj | 4 +- src/SMAPI/packages.config | 1 - 7 files changed, 149 insertions(+), 11 deletions(-) create mode 100644 src/SMAPI/Framework/Reflection/InterfaceProxyBuilder.cs diff --git a/build/common.targets b/build/common.targets index 15c935e3..aa11344e 100644 --- a/build/common.targets +++ b/build/common.targets @@ -86,7 +86,6 @@ - diff --git a/build/prepare-install-package.targets b/build/prepare-install-package.targets index f8262271..dde2ff0a 100644 --- a/build/prepare-install-package.targets +++ b/build/prepare-install-package.targets @@ -22,7 +22,6 @@ - @@ -38,7 +37,6 @@ - diff --git a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs index 9574a632..ea0dbb38 100644 --- a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using System.Linq; -using ImpromptuInterface; +using StardewModdingAPI.Framework.Reflection; namespace StardewModdingAPI.Framework.ModHelpers { @@ -19,6 +19,9 @@ namespace StardewModdingAPI.Framework.ModHelpers /// 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 @@ -26,11 +29,13 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Construct an instance. /// The unique ID of the relevant mod. /// The underlying mod 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, IMonitor monitor) + public ModRegistryHelper(string modID, ModRegistry registry, InterfaceProxyBuilder proxyBuilder, IMonitor monitor) : base(modID) { this.Registry = registry; + this.ProxyBuilder = proxyBuilder; this.Monitor = monitor; } @@ -94,7 +99,7 @@ namespace StardewModdingAPI.Framework.ModHelpers // get API of type if (api is TInterface castApi) return castApi; - return api.ActLike(); + return this.ProxyBuilder.CreateProxy(api, this.ModID, uniqueID); } } } 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/Program.cs b/src/SMAPI/Program.cs index 786549fe..7eda9c66 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -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 @@ -706,7 +707,7 @@ namespace StardewModdingAPI 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, monitor); + IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry, proxyBuilder, monitor); 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); } diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 026ac106..0e8ccaa3 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -53,9 +53,6 @@ icon.ico - - ..\packages\ImpromptuInterface.6.2.2\lib\net40\ImpromptuInterface.dll - ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.dll True @@ -116,6 +113,7 @@ + diff --git a/src/SMAPI/packages.config b/src/SMAPI/packages.config index 60be6881..98d742c7 100644 --- a/src/SMAPI/packages.config +++ b/src/SMAPI/packages.config @@ -1,6 +1,5 @@  - \ No newline at end of file From 4c3ae950c6be0840e98d1328eeca9effca9c3807 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 15 Dec 2017 23:28:16 -0500 Subject: [PATCH 12/12] rm unneeded assembly references (#409) --- build/prepare-install-package.targets | 1 - .../StardewModdingAPI.Mods.ConsoleCommands.csproj | 4 ---- src/SMAPI/StardewModdingAPI.csproj | 7 ------- 3 files changed, 12 deletions(-) diff --git a/build/prepare-install-package.targets b/build/prepare-install-package.targets index dde2ff0a..f2a2b23c 100644 --- a/build/prepare-install-package.targets +++ b/build/prepare-install-package.targets @@ -23,7 +23,6 @@ - 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/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 0e8ccaa3..f76ac439 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -79,13 +79,6 @@ True - - - - - True - -