add prototype of mod-provided APIs (#409)
This commit is contained in:
parent
971aff8368
commit
2c909f26fc
|
@ -30,6 +30,9 @@ namespace StardewModdingAPI.Framework
|
||||||
/// <summary>The mod instance (if it was loaded).</summary>
|
/// <summary>The mod instance (if it was loaded).</summary>
|
||||||
IMod Mod { get; }
|
IMod Mod { get; }
|
||||||
|
|
||||||
|
/// <summary>The mod-provided API (if any).</summary>
|
||||||
|
IModProvidedApi Api { get; }
|
||||||
|
|
||||||
|
|
||||||
/*********
|
/*********
|
||||||
** Public methods
|
** Public methods
|
||||||
|
@ -42,6 +45,7 @@ namespace StardewModdingAPI.Framework
|
||||||
|
|
||||||
/// <summary>Set the mod instance.</summary>
|
/// <summary>Set the mod instance.</summary>
|
||||||
/// <param name="mod">The mod instance to set.</param>
|
/// <param name="mod">The mod instance to set.</param>
|
||||||
IModMetadata SetMod(IMod mod);
|
/// <param name="api">The mod-provided API (if any).</param>
|
||||||
|
IModMetadata SetMod(IMod mod, IModProvidedApi api);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,5 +45,11 @@ namespace StardewModdingAPI.Framework.ModHelpers
|
||||||
{
|
{
|
||||||
return this.Registry.Get(uniqueID) != null;
|
return this.Registry.Get(uniqueID) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Get the API provided by a mod, or <c>null</c> if it has none. This signature requires using the <see cref="IModHelper.Reflection"/> API to access the API's properties and methods.</summary>
|
||||||
|
public IModProvidedApi GetApi(string uniqueID)
|
||||||
|
{
|
||||||
|
return this.Registry.Get(uniqueID)?.Api;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,9 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||||
/// <summary>The mod instance (if it was loaded).</summary>
|
/// <summary>The mod instance (if it was loaded).</summary>
|
||||||
public IMod Mod { get; private set; }
|
public IMod Mod { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>The mod-provided API (if any).</summary>
|
||||||
|
public IModProvidedApi Api { get; private set; }
|
||||||
|
|
||||||
|
|
||||||
/*********
|
/*********
|
||||||
** Public methods
|
** Public methods
|
||||||
|
@ -59,9 +62,11 @@ namespace StardewModdingAPI.Framework.ModLoading
|
||||||
|
|
||||||
/// <summary>Set the mod instance.</summary>
|
/// <summary>Set the mod instance.</summary>
|
||||||
/// <param name="mod">The mod instance to set.</param>
|
/// <param name="mod">The mod instance to set.</param>
|
||||||
public IModMetadata SetMod(IMod mod)
|
/// <param name="api">The mod-provided API (if any).</param>
|
||||||
|
public IModMetadata SetMod(IMod mod, IModProvidedApi api)
|
||||||
{
|
{
|
||||||
this.Mod = mod;
|
this.Mod = mod;
|
||||||
|
this.Api = api;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
namespace StardewModdingAPI
|
||||||
|
{
|
||||||
|
/// <summary>An API provided by a mod for other mods to use.</summary>
|
||||||
|
/// <remarks>This is a marker interface. Each mod can only have one implementation of <see cref="IModProvidedApi"/>.</remarks>
|
||||||
|
public interface IModProvidedApi { }
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace StardewModdingAPI
|
namespace StardewModdingAPI
|
||||||
{
|
{
|
||||||
|
@ -16,5 +16,9 @@ namespace StardewModdingAPI
|
||||||
/// <summary>Get whether a mod has been loaded.</summary>
|
/// <summary>Get whether a mod has been loaded.</summary>
|
||||||
/// <param name="uniqueID">The mod's unique ID.</param>
|
/// <param name="uniqueID">The mod's unique ID.</param>
|
||||||
bool IsLoaded(string uniqueID);
|
bool IsLoaded(string uniqueID);
|
||||||
|
|
||||||
|
/// <summary>Get the API provided by a mod, or <c>null</c> if it has none. This signature requires using the <see cref="IModHelper.Reflection"/> API to access the API's properties and methods.</summary>
|
||||||
|
/// <param name="uniqueID">The mod's unique ID.</param>
|
||||||
|
IModProvidedApi GetApi(string uniqueID);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -696,55 +696,34 @@ namespace StardewModdingAPI
|
||||||
continue;
|
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
|
// initialise mod
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// get implementation
|
// init mod helpers
|
||||||
TypeInfo modEntryType = modAssembly.DefinedTypes.First(type => typeof(Mod).IsAssignableFrom(type) && !type.IsAbstract);
|
|
||||||
Mod mod = (Mod)modAssembly.CreateInstance(modEntryType.ToString());
|
|
||||||
if (mod == null)
|
|
||||||
{
|
|
||||||
TrackSkip(metadata, "its entry class couldn't be instantiated.");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// inject data
|
|
||||||
{
|
|
||||||
IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName);
|
IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName);
|
||||||
|
IModHelper modHelper;
|
||||||
|
{
|
||||||
ICommandHelper commandHelper = new CommandHelper(manifest.UniqueID, metadata.DisplayName, this.CommandManager);
|
ICommandHelper commandHelper = new CommandHelper(manifest.UniqueID, metadata.DisplayName, this.CommandManager);
|
||||||
IContentHelper contentHelper = new ContentHelper(contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor);
|
IContentHelper contentHelper = new ContentHelper(contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor);
|
||||||
IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, metadata.DisplayName, this.Reflection, this.DeprecationManager);
|
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);
|
||||||
ITranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentManager.GetLocale(), contentManager.GetCurrentLanguage());
|
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);
|
||||||
mod.ModManifest = manifest;
|
|
||||||
mod.Helper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, translationHelper);
|
|
||||||
mod.Monitor = monitor;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// track mod
|
||||||
metadata.SetMod(mod);
|
metadata.SetMod(mod, api);
|
||||||
this.ModRegistry.Add(metadata);
|
this.ModRegistry.Add(metadata);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
@ -854,6 +833,105 @@ namespace StardewModdingAPI
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Load a mod's entry class.</summary>
|
||||||
|
/// <param name="modAssembly">The mod assembly.</param>
|
||||||
|
/// <param name="onError">A callback invoked when loading fails.</param>
|
||||||
|
/// <param name="mod">The loaded instance.</param>
|
||||||
|
private bool TryLoadModEntry(Assembly modAssembly, Action<string> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Load a mod's <see cref="IModProvidedApi"/> implementation.</summary>
|
||||||
|
/// <param name="modAssembly">The mod assembly.</param>
|
||||||
|
/// <param name="modHelper">The mod's helper instance.</param>
|
||||||
|
/// <param name="monitor">The mod's monitor instance.</param>
|
||||||
|
/// <param name="onError">A callback invoked when loading fails.</param>
|
||||||
|
/// <param name="api">The loaded instance.</param>
|
||||||
|
private bool TryLoadModProvidedApi(Assembly modAssembly, IModHelper modHelper, IMonitor monitor, Action<string> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Reload translations for all mods.</summary>
|
/// <summary>Reload translations for all mods.</summary>
|
||||||
private void ReloadTranslations()
|
private void ReloadTranslations()
|
||||||
{
|
{
|
||||||
|
|
|
@ -114,6 +114,7 @@
|
||||||
<Compile Include="IReflectedField.cs" />
|
<Compile Include="IReflectedField.cs" />
|
||||||
<Compile Include="IReflectedMethod.cs" />
|
<Compile Include="IReflectedMethod.cs" />
|
||||||
<Compile Include="IReflectedProperty.cs" />
|
<Compile Include="IReflectedProperty.cs" />
|
||||||
|
<Compile Include="IModProvidedApi.cs" />
|
||||||
<Compile Include="Metadata\CoreAssets.cs" />
|
<Compile Include="Metadata\CoreAssets.cs" />
|
||||||
<Compile Include="ContentSource.cs" />
|
<Compile Include="ContentSource.cs" />
|
||||||
<Compile Include="Events\ContentEvents.cs" />
|
<Compile Include="Events\ContentEvents.cs" />
|
||||||
|
|
Loading…
Reference in New Issue