From e8da8fff5163eacd6ae7870eaa8c7dbc8285e3e7 Mon Sep 17 00:00:00 2001 From: Khloe Leclair Date: Mon, 26 Sep 2022 15:18:36 -0400 Subject: [PATCH 1/3] Initial work on a way for mods to return specific API instances to specific mods. --- .../Framework/ModHelpers/ModRegistryHelper.cs | 53 ++++++++++++++++--- src/SMAPI/IMod.cs | 6 +++ src/SMAPI/Mod.cs | 6 +++ 3 files changed, 59 insertions(+), 6 deletions(-) diff --git a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs index 348ba225..9ad3e3ae 100644 --- a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs @@ -1,5 +1,7 @@ +using System; using System.Collections.Generic; using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Internal; namespace StardewModdingAPI.Framework.ModHelpers { @@ -15,8 +17,8 @@ namespace StardewModdingAPI.Framework.ModHelpers /// 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(); + /// The APIs accessed by this instance. + private readonly Dictionary AccessedModApis = new(); /// Generates proxy classes to access mod APIs through an arbitrary interface. private readonly IInterfaceProxyFactory ProxyFactory; @@ -66,11 +68,50 @@ namespace StardewModdingAPI.Framework.ModHelpers return null; } - // get raw API + // get our cached API if one is available 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}."); - return mod?.Api; + if (mod == null) + return null; + + if (this.AccessedModApis.ContainsKey(mod.Manifest.UniqueID)) + { + return this.AccessedModApis[mod.Manifest.UniqueID]; + } + + object? api; + + // safely request a specific API instance + try + { + api = mod.Mod?.GetApi(this.Mod.Manifest); + if (api != null && !api.GetType().IsPublic) + { + api = null; + this.Monitor.Log($"{mod.DisplayName} provided a specific API instance with a non-public type. This isn't currently supported, so the specific API won't be available to the requesting mod.", LogLevel.Warn); + } + + if (api != null) + this.Monitor.Log($"Accessed specific mod-provided API ({api.GetType().FullName}) for {mod.DisplayName}."); + } + catch (Exception ex) + { + this.Monitor.Log($"Failed loading specific mod-provided API for {mod.DisplayName}. Integrations with other mods may not work. Error: {ex.GetLogSummary()}", LogLevel.Error); + api = null; + } + + // fall back to the generic API instance + if (api == null) + { + api = mod.Api; + if (api != null) + { + this.Monitor.Log($"Accessed mod-provided API for {mod.DisplayName}."); + } + } + + // cache the API instance and return it + this.AccessedModApis[mod.Manifest.UniqueID] = api; + return api; } /// diff --git a/src/SMAPI/IMod.cs b/src/SMAPI/IMod.cs index b81ba0e3..6041bf66 100644 --- a/src/SMAPI/IMod.cs +++ b/src/SMAPI/IMod.cs @@ -25,5 +25,11 @@ namespace StardewModdingAPI /// Get an API that other mods can access. This is always called after . object? GetApi(); + + /// Get an API that a specific other mod can access. This method is called the first time the other mod calls for this mod. + /// The other mod's manifest. + /// Returns an API for another mod, or null if the other mod should use the general API returned from . + object? GetApi(IManifest manifest); + } } diff --git a/src/SMAPI/Mod.cs b/src/SMAPI/Mod.cs index f764752b..1a5f5594 100644 --- a/src/SMAPI/Mod.cs +++ b/src/SMAPI/Mod.cs @@ -30,6 +30,12 @@ namespace StardewModdingAPI return null; } + /// + public virtual object? GetApi(IManifest manifest) + { + return null; + } + /// Release or reset unmanaged resources. public void Dispose() { From a565ac9405a95d24f7cf945228935107e91bb89f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 8 Oct 2022 19:59:21 -0400 Subject: [PATCH 2/3] make GetApi methods mutually exclusive & improve docs --- .../Framework/ModHelpers/ModRegistryHelper.cs | 56 +++++++++---------- src/SMAPI/Framework/SCore.cs | 5 ++ src/SMAPI/IMod.cs | 12 ++-- 3 files changed, 37 insertions(+), 36 deletions(-) diff --git a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs index 9ad3e3ae..8cc73481 100644 --- a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs @@ -68,49 +68,43 @@ namespace StardewModdingAPI.Framework.ModHelpers return null; } - // get our cached API if one is available + // get the target mod IModMetadata? mod = this.Registry.Get(uniqueID); if (mod == null) return null; - if (this.AccessedModApis.ContainsKey(mod.Manifest.UniqueID)) + // fetch API + if (!this.AccessedModApis.TryGetValue(mod.Manifest.UniqueID, out object? api)) { - return this.AccessedModApis[mod.Manifest.UniqueID]; - } + // if the target has a global API, this is mutually exclusive with per-mod APIs + if (mod.Api != null) + api = mod.Api; - object? api; - - // safely request a specific API instance - try - { - api = mod.Mod?.GetApi(this.Mod.Manifest); - if (api != null && !api.GetType().IsPublic) + // else try to get a per-mod API + else { - api = null; - this.Monitor.Log($"{mod.DisplayName} provided a specific API instance with a non-public type. This isn't currently supported, so the specific API won't be available to the requesting mod.", LogLevel.Warn); + try + { + api = mod.Mod?.GetApi(this.Mod.Manifest); + if (api != null && !api.GetType().IsPublic) + { + api = null; + this.Monitor.Log($"{mod.DisplayName} provides a per-mod API instance with a non-public type. This isn't currently supported, so the API won't be available to other mods.", LogLevel.Warn); + } + } + catch (Exception ex) + { + this.Monitor.Log($"Failed loading the per-mod API instance from {mod.DisplayName}. Integrations with other mods may not work. Error: {ex.GetLogSummary()}", LogLevel.Error); + api = null; + } } + // cache & log API access + this.AccessedModApis[mod.Manifest.UniqueID] = api; if (api != null) - this.Monitor.Log($"Accessed specific mod-provided API ({api.GetType().FullName}) for {mod.DisplayName}."); - } - catch (Exception ex) - { - this.Monitor.Log($"Failed loading specific mod-provided API for {mod.DisplayName}. Integrations with other mods may not work. Error: {ex.GetLogSummary()}", LogLevel.Error); - api = null; + this.Monitor.Log($"Accessed mod-provided API ({api.GetType().FullName}) for {mod.DisplayName}."); } - // fall back to the generic API instance - if (api == null) - { - api = mod.Api; - if (api != null) - { - this.Monitor.Log($"Accessed mod-provided API for {mod.DisplayName}."); - } - } - - // cache the API instance and return it - this.AccessedModApis[mod.Manifest.UniqueID] = api; return api; } diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 16ff2537..4ba0dd9c 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -1779,6 +1779,11 @@ namespace StardewModdingAPI.Framework { this.Monitor.Log($"Failed loading mod-provided API for {metadata.DisplayName}. Integrations with other mods may not work. Error: {ex.GetLogSummary()}", LogLevel.Error); } + + // validate mod doesn't implement both GetApi() and GetApi(mod) + if (metadata.Api != null && metadata.Mod!.GetType().GetMethod(nameof(Mod.GetApi), new Type[] { typeof(IManifest) })!.DeclaringType != typeof(Mod)) + metadata.LogAsMod($"Mod implements both {nameof(Mod.GetApi)}() and {nameof(Mod.GetApi)}({nameof(IManifest)}), which isn't allowed. The latter will be ignored.", LogLevel.Error); + Context.HeuristicModsRunningCode.TryPop(out _); } diff --git a/src/SMAPI/IMod.cs b/src/SMAPI/IMod.cs index 6041bf66..4576246a 100644 --- a/src/SMAPI/IMod.cs +++ b/src/SMAPI/IMod.cs @@ -23,13 +23,15 @@ namespace StardewModdingAPI /// Provides simplified APIs for writing mods. void Entry(IModHelper helper); - /// Get an API that other mods can access. This is always called after . + /// Get an API that other mods can access. This is always called after , and is only called once even if multiple mods access it. + /// You can implement to provide one instance to all mods, or to provide a separate instance per mod. These are mutually exclusive, so you can only implement one of them. + /// Returns the API instance, or null if the mod has no API. object? GetApi(); - /// Get an API that a specific other mod can access. This method is called the first time the other mod calls for this mod. - /// The other mod's manifest. - /// Returns an API for another mod, or null if the other mod should use the general API returned from . + /// Get an API that other mods can access. This is always called after , and is called once per mod that accesses the API (even if they access it multiple times). + /// The manifest for the mod accessing the API. + /// Returns the API instance, or null if the mod has no API. Note that the manifest is provided for informational purposes only, and that denying API access to specific mods is strongly discouraged and may be considered abusive. + /// object? GetApi(IManifest manifest); - } } From 8d6670cfc8abf7e71197d2f621314fb04a0543b8 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 8 Oct 2022 20:33:01 -0400 Subject: [PATCH 3/3] pass mod info to GetApi instead --- src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs | 2 +- src/SMAPI/IMod.cs | 6 +++--- src/SMAPI/Mod.cs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs index 8cc73481..93edd597 100644 --- a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs @@ -85,7 +85,7 @@ namespace StardewModdingAPI.Framework.ModHelpers { try { - api = mod.Mod?.GetApi(this.Mod.Manifest); + api = mod.Mod?.GetApi(this.Mod); if (api != null && !api.GetType().IsPublic) { api = null; diff --git a/src/SMAPI/IMod.cs b/src/SMAPI/IMod.cs index 4576246a..19d01311 100644 --- a/src/SMAPI/IMod.cs +++ b/src/SMAPI/IMod.cs @@ -24,14 +24,14 @@ namespace StardewModdingAPI void Entry(IModHelper helper); /// Get an API that other mods can access. This is always called after , and is only called once even if multiple mods access it. - /// You can implement to provide one instance to all mods, or to provide a separate instance per mod. These are mutually exclusive, so you can only implement one of them. + /// You can implement to provide one instance to all mods, or to provide a separate instance per mod. These are mutually exclusive, so you can only implement one of them. /// Returns the API instance, or null if the mod has no API. object? GetApi(); /// Get an API that other mods can access. This is always called after , and is called once per mod that accesses the API (even if they access it multiple times). - /// The manifest for the mod accessing the API. + /// The mod accessing the API. /// Returns the API instance, or null if the mod has no API. Note that the manifest is provided for informational purposes only, and that denying API access to specific mods is strongly discouraged and may be considered abusive. /// - object? GetApi(IManifest manifest); + object? GetApi(IModInfo mod); } } diff --git a/src/SMAPI/Mod.cs b/src/SMAPI/Mod.cs index 1a5f5594..01157886 100644 --- a/src/SMAPI/Mod.cs +++ b/src/SMAPI/Mod.cs @@ -31,7 +31,7 @@ namespace StardewModdingAPI } /// - public virtual object? GetApi(IManifest manifest) + public virtual object? GetApi(IModInfo mod) { return null; }