Merge pull request #874 from KhloeLeclair/specific_apis

Add `IMod.GetApi(IManifest manifest)`
This commit is contained in:
Jesse Plamondon-Willard 2022-10-08 20:45:49 -04:00 committed by GitHub
commit 5ef726be92
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 61 additions and 7 deletions

View File

@ -1,5 +1,7 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Reflection;
using StardewModdingAPI.Internal;
namespace StardewModdingAPI.Framework.ModHelpers namespace StardewModdingAPI.Framework.ModHelpers
{ {
@ -15,8 +17,8 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <summary>Encapsulates monitoring and logging for the mod.</summary> /// <summary>Encapsulates monitoring and logging for the mod.</summary>
private readonly IMonitor Monitor; private readonly IMonitor Monitor;
/// <summary>The mod IDs for APIs accessed by this instanced.</summary> /// <summary>The APIs accessed by this instance.</summary>
private readonly HashSet<string> AccessedModApis = new(); private readonly Dictionary<string, object?> AccessedModApis = new();
/// <summary>Generates proxy classes to access mod APIs through an arbitrary interface.</summary> /// <summary>Generates proxy classes to access mod APIs through an arbitrary interface.</summary>
private readonly IInterfaceProxyFactory ProxyFactory; private readonly IInterfaceProxyFactory ProxyFactory;
@ -66,11 +68,44 @@ namespace StardewModdingAPI.Framework.ModHelpers
return null; return null;
} }
// get raw API // get the target mod
IModMetadata? mod = this.Registry.Get(uniqueID); IModMetadata? mod = this.Registry.Get(uniqueID);
if (mod?.Api != null && this.AccessedModApis.Add(mod.Manifest.UniqueID)) if (mod == null)
this.Monitor.Log($"Accessed mod-provided API for {mod.DisplayName}."); return null;
return mod?.Api;
// fetch API
if (!this.AccessedModApis.TryGetValue(mod.Manifest.UniqueID, out object? api))
{
// if the target has a global API, this is mutually exclusive with per-mod APIs
if (mod.Api != null)
api = mod.Api;
// else try to get a per-mod API
else
{
try
{
api = mod.Mod?.GetApi(this.Mod);
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 mod-provided API ({api.GetType().FullName}) for {mod.DisplayName}.");
}
return api;
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@ -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); 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 _); Context.HeuristicModsRunningCode.TryPop(out _);
} }

View File

@ -23,7 +23,15 @@ namespace StardewModdingAPI
/// <param name="helper">Provides simplified APIs for writing mods.</param> /// <param name="helper">Provides simplified APIs for writing mods.</param>
void Entry(IModHelper helper); void Entry(IModHelper helper);
/// <summary>Get an API that other mods can access. This is always called after <see cref="Entry"/>.</summary> /// <summary>Get an <a href="https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Integrations">API that other mods can access</a>. This is always called after <see cref="Entry"/>, and is only called once even if multiple mods access it.</summary>
/// <remarks>You can implement <see cref="GetApi()"/> to provide one instance to all mods, or <see cref="GetApi(IModInfo)"/> to provide a separate instance per mod. These are mutually exclusive, so you can only implement one of them.</remarks>
/// <remarks>Returns the API instance, or <c>null</c> if the mod has no API.</remarks>
object? GetApi(); object? GetApi();
/// <summary>Get an <a href="https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Integrations">API that other mods can access</a>. This is always called after <see cref="Entry"/>, and is called once per mod that accesses the API (even if they access it multiple times).</summary>
/// <param name="mod">The mod accessing the API.</param>
/// <remarks>Returns the API instance, or <c>null</c> 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.</remarks>
/// <inheritdoc cref="GetApi()" include="/Remarks" />
object? GetApi(IModInfo mod);
} }
} }

View File

@ -30,6 +30,12 @@ namespace StardewModdingAPI
return null; return null;
} }
/// <inheritdoc />
public virtual object? GetApi(IModInfo mod)
{
return null;
}
/// <summary>Release or reset unmanaged resources.</summary> /// <summary>Release or reset unmanaged resources.</summary>
public void Dispose() public void Dispose()
{ {