From 8776d1afa6dce054f3bc7cb421c86f3e2fe06ab3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 10 Dec 2017 18:05:18 -0500 Subject: adjust reflection API to correctly reflect what it does (#410) --- src/SMAPI/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/SMAPI/Program.cs') diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 3ba35e43..7bfb0abd 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -728,7 +728,7 @@ namespace StardewModdingAPI 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); + 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()); -- cgit From 69c9ab0ecd184e4706a8e6394b38fa592cb808d0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 11 Dec 2017 21:29:39 -0500 Subject: trace mods with no update keys, tweak update-check logging --- docs/release-notes.md | 1 + src/SMAPI/Program.cs | 160 ++++++++++++++++++++++++++------------------------ 2 files changed, 84 insertions(+), 77 deletions(-) (limited to 'src/SMAPI/Program.cs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 57d9c480..0e2477f4 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,7 @@ ## 2.3 * For modders: * 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. * Fixed error when using the reflection API accesses with a property with either `get` and `set` missing. * Fixed issue where a mod could change the cursor position reported to other mods. diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 7bfb0abd..8bc2c675 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -500,12 +500,11 @@ namespace StardewModdingAPI { // create client WebApiClient client = new WebApiClient(this.Settings.WebApiBaseUrl, Constants.ApiVersion); + this.Monitor.Log("Checking for updates...", LogLevel.Trace); // check SMAPI version try { - this.Monitor.Log("Checking for SMAPI update...", LogLevel.Trace); - ModInfoModel response = client.GetModInfo($"GitHub:{this.Settings.GitHubProjectName}").Single().Value; if (response.Error != null) { @@ -515,7 +514,7 @@ namespace StardewModdingAPI else if (new SemanticVersion(response.Version).IsNewerThan(Constants.ApiVersion)) this.Monitor.Log($"You can update SMAPI to {response.Version}: {response.Url}", LogLevel.Alert); else - this.VerboseLog(" OK."); + this.Monitor.Log(" SMAPI okay.", LogLevel.Trace); } catch (Exception ex) { @@ -527,95 +526,102 @@ namespace StardewModdingAPI } // check mod versions - try + if (mods.Any()) { - // log issues - if (this.Settings.VerboseLogging) + try { - this.VerboseLog("Validating mod update keys..."); - foreach (IModMetadata mod in mods) + // prepare update keys + Dictionary modsByKey = + ( + from mod in mods + where mod.Manifest?.UpdateKeys != null + from key in mod.Manifest.UpdateKeys + select new { key, mod } + ) + .GroupBy(p => p.key, StringComparer.InvariantCultureIgnoreCase) + .ToDictionary( + group => group.Key, + group => group.Select(p => p.mod).ToArray(), + StringComparer.InvariantCultureIgnoreCase + ); + + // report update keys { - if (mod.Manifest == null) - this.VerboseLog($" {mod.DisplayName}: no manifest."); - else if (mod.Manifest.UpdateKeys == null || !mod.Manifest.UpdateKeys.Any()) - this.VerboseLog($" {mod.DisplayName}: no update keys."); + IModMetadata[] modsWithoutKeys = ( + from mod in mods + where + mod.Manifest != null + && (mod.Manifest.UpdateKeys == null || !mod.Manifest.UpdateKeys.Any()) + && (mod.Manifest?.UniqueID != "SMAPI.ConsoleCommands" && mod.Manifest?.UniqueID != "SMAPI.TrainerMod") + orderby mod.DisplayName + select mod + ).ToArray(); + + string message = $"Checking {modsByKey.Count} mod update keys."; + if (modsWithoutKeys.Any()) + message += $" {modsWithoutKeys.Length} mods have no update keys: {string.Join(", ", modsWithoutKeys.Select(p => p.DisplayName))}."; + this.Monitor.Log($" {message}", LogLevel.Trace); } - } - // prepare update keys - Dictionary modsByKey = - ( - from mod in mods - where mod.Manifest?.UpdateKeys != null - from key in mod.Manifest.UpdateKeys - select new { key, mod } - ) - .GroupBy(p => p.key, StringComparer.InvariantCultureIgnoreCase) - .ToDictionary( - group => group.Key, - group => group.Select(p => p.mod).ToArray(), - StringComparer.InvariantCultureIgnoreCase - ); + // fetch results + var results = + ( + from entry in client.GetModInfo(modsByKey.Keys.ToArray()) + from mod in modsByKey[entry.Key] + orderby mod.DisplayName + select new { entry.Key, Mod = mod, Info = entry.Value } + ) + .ToArray(); + + // extract latest versions + IDictionary updatesByMod = new Dictionary(); + foreach (var result in results) + { + IModMetadata mod = result.Mod; + ModInfoModel info = result.Info; - // fetch results - this.Monitor.Log($"Checking for updates to {modsByKey.Keys.Count} keys...", LogLevel.Trace); - var results = - ( - from entry in client.GetModInfo(modsByKey.Keys.ToArray()) - from mod in modsByKey[entry.Key] - orderby mod.DisplayName - select new { entry.Key, Mod = mod, Info = entry.Value } - ) - .ToArray(); - - // extract latest versions - IDictionary updatesByMod = new Dictionary(); - foreach (var result in results) - { - IModMetadata mod = result.Mod; - ModInfoModel info = result.Info; + // handle error + if (info.Error != null) + { + this.Monitor.Log($" {mod.DisplayName} ({result.Key}): update error: {info.Error}", LogLevel.Trace); + continue; + } - // handle error - if (info.Error != null) - { - this.Monitor.Log($" {mod.DisplayName} ({result.Key}): update error: {info.Error}", LogLevel.Trace); - continue; + // track update + ISemanticVersion localVersion = mod.DataRecord != null + ? new SemanticVersion(mod.DataRecord.GetLocalVersionForUpdateChecks(mod.Manifest.Version.ToString())) + : mod.Manifest.Version; + ISemanticVersion latestVersion = new SemanticVersion(mod.DataRecord != null + ? mod.DataRecord.GetRemoteVersionForUpdateChecks(new SemanticVersion(info.Version).ToString()) + : info.Version + ); + bool isUpdate = latestVersion.IsNewerThan(localVersion); + this.VerboseLog($" {mod.DisplayName} ({result.Key}): {(isUpdate ? $"{mod.Manifest.Version}{(!localVersion.Equals(mod.Manifest.Version) ? $" [{localVersion}]" : "")} => {info.Version}{(!latestVersion.Equals(new SemanticVersion(info.Version)) ? $" [{latestVersion}]" : "")}" : "okay")}."); + if (isUpdate) + { + if (!updatesByMod.TryGetValue(mod, out ModInfoModel other) || latestVersion.IsNewerThan(other.Version)) + updatesByMod[mod] = info; + } } - // track update - ISemanticVersion localVersion = mod.DataRecord != null - ? new SemanticVersion(mod.DataRecord.GetLocalVersionForUpdateChecks(mod.Manifest.Version.ToString())) - : mod.Manifest.Version; - ISemanticVersion latestVersion = new SemanticVersion(mod.DataRecord != null - ? mod.DataRecord.GetRemoteVersionForUpdateChecks(new SemanticVersion(info.Version).ToString()) - : info.Version - ); - bool isUpdate = latestVersion.IsNewerThan(localVersion); - this.VerboseLog($" {mod.DisplayName} ({result.Key}): {(isUpdate ? $"{mod.Manifest.Version}{(!localVersion.Equals(mod.Manifest.Version) ? $" [{localVersion}]" : "")} => {info.Version}{(!latestVersion.Equals(new SemanticVersion(info.Version)) ? $" [{latestVersion}]" : "")}" : "OK")}."); - if (isUpdate) + // output + if (updatesByMod.Any()) { - if (!updatesByMod.TryGetValue(mod, out ModInfoModel other) || latestVersion.IsNewerThan(other.Version)) - updatesByMod[mod] = info; + this.Monitor.Newline(); + this.Monitor.Log($"You can update {updatesByMod.Count} mod{(updatesByMod.Count != 1 ? "s" : "")}:", LogLevel.Alert); + foreach (var entry in updatesByMod.OrderBy(p => p.Key.DisplayName)) + this.Monitor.Log($" {entry.Key.DisplayName} {entry.Value.Version}: {entry.Value.Url}", LogLevel.Alert); } } - - // output - if (updatesByMod.Any()) + catch (Exception ex) { - this.Monitor.Newline(); - this.Monitor.Log($"You can update {updatesByMod.Count} mod{(updatesByMod.Count != 1 ? "s" : "")}:", LogLevel.Alert); - foreach (var entry in updatesByMod.OrderBy(p => p.Key.DisplayName)) - this.Monitor.Log($" {entry.Key.DisplayName} {entry.Value.Version}: {entry.Value.Url}", LogLevel.Alert); + this.Monitor.Log("Couldn't check for new mod versions. This won't affect your game, but you won't be notified of mod updates if this keeps happening.", LogLevel.Warn); + this.Monitor.Log(ex is WebException && ex.InnerException == null + ? ex.Message + : ex.ToString() + ); } } - catch (Exception ex) - { - this.Monitor.Log("Couldn't check for new mod versions. This won't affect your game, but you won't be notified of mod updates if this keeps happening.", LogLevel.Warn); - this.Monitor.Log(ex is WebException && ex.InnerException == null - ? ex.Message - : ex.ToString() - ); - } }).Start(); } -- cgit From 971aff8368a8a2c196d942984926efc2f80cc216 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 11 Dec 2017 22:29:56 -0500 Subject: generalise internal mod registry (#409) --- src/SMAPI/Framework/DeprecationManager.cs | 4 +- .../Framework/ModHelpers/ModRegistryHelper.cs | 9 ++-- src/SMAPI/Framework/ModRegistry.cs | 59 ++++++++-------------- src/SMAPI/Program.cs | 10 ++-- 4 files changed, 32 insertions(+), 50 deletions(-) (limited to 'src/SMAPI/Program.cs') 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 - ****/ + /// 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; + } + /// Get metadata for all loaded mods. - public IEnumerable GetAll() + public IEnumerable GetAll() { - return this.Mods.Select(p => p.Manifest); + 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)); - } - - /// 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); + return this.GetAll().FirstOrDefault(p => p.Manifest.UniqueID.Trim().Equals(uniqueID, StringComparison.InvariantCultureIgnoreCase)); } - /// 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>(); -- cgit From 2c909f26fcf48fc1de7f3b23f5f83d28d4a5e253 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 11 Dec 2017 23:33:10 -0500 Subject: 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 (limited to 'src/SMAPI/Program.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 @@ + -- cgit From 7d644aeabee63c0d51d4e89360d2fdab0e51b8be Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 12 Dec 2017 00:09:28 -0500 Subject: 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 (limited to 'src/SMAPI/Program.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 @@ - -- cgit From d04cacbdd0729140e4d8e93323ba66ee90ff9d2a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 12 Dec 2017 00:16:34 -0500 Subject: log mod-provided API access (#409) --- src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs | 15 +++++++++++++-- src/SMAPI/Program.cs | 2 +- 2 files changed, 14 insertions(+), 3 deletions(-) (limited to 'src/SMAPI/Program.cs') 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); } -- cgit From e00424068f3da7c4f91187872e96c90fa61e47db Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 12 Dec 2017 01:33:11 -0500 Subject: 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(+) (limited to 'src/SMAPI/Program.cs') 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. -- cgit From 21fd2d1e39a6a94758f6298c2da52cd46cffdfcd Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 15 Dec 2017 21:37:08 -0500 Subject: emit proxy classes directly to simplify crossplatform compatibility (#409) --- build/common.targets | 1 - build/prepare-install-package.targets | 2 - .../Framework/ModHelpers/ModRegistryHelper.cs | 11 +- .../Framework/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 (limited to 'src/SMAPI/Program.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 -- cgit