unify logic for loading content packs & mods to simplify common validation

This commit is contained in:
Jesse Plamondon-Willard 2018-09-18 18:47:32 -04:00
parent 8b2fd0bab7
commit 2cd0c8130c
1 changed files with 162 additions and 147 deletions

View File

@ -693,150 +693,23 @@ namespace StardewModdingAPI.Framework
{
this.Monitor.Log("Loading mods...", LogLevel.Trace);
HashSet<string> suppressUpdateChecks = new HashSet<string>(this.Settings.SuppressUpdateChecks, StringComparer.InvariantCultureIgnoreCase);
IDictionary<IModMetadata, string[]> skippedMods = new Dictionary<IModMetadata, string[]>();
void TrackSkip(IModMetadata mod, string userReasonPhrase, string devReasonPhrase = null) => skippedMods[mod] = new[] { userReasonPhrase, devReasonPhrase };
// load content packs
foreach (IModMetadata metadata in mods.Where(p => p.IsContentPack))
{
this.Monitor.Log($" {metadata.DisplayName} (content pack, {PathUtilities.GetRelativePath(this.ModsPath, metadata.DirectoryPath)})...", LogLevel.Trace);
// show warning for missing update key
if (metadata.HasManifest() && !metadata.HasValidUpdateKeys())
metadata.SetWarning(ModWarning.NoUpdateKeys);
// validate status
if (metadata.Status == ModMetadataStatus.Failed)
{
this.Monitor.Log($" Failed: {metadata.Error}", LogLevel.Trace);
TrackSkip(metadata, metadata.Error);
continue;
}
// load mod as content pack
IManifest manifest = metadata.Manifest;
IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName);
IContentHelper contentHelper = new ContentHelper(this.ContentCore, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor);
IContentPack contentPack = new ContentPack(metadata.DirectoryPath, manifest, contentHelper, jsonHelper);
metadata.SetMod(contentPack, monitor);
this.ModRegistry.Add(metadata);
}
IModMetadata[] loadedContentPacks = this.ModRegistry.GetAll(assemblyMods: false).ToArray();
// load mods
IDictionary<IModMetadata, Tuple<string, string>> skippedMods = new Dictionary<IModMetadata, Tuple<string, string>>();
using (AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.Platform, this.Monitor))
{
// get content packs by mod ID
IDictionary<string, IContentPack[]> contentPacksByModID =
loadedContentPacks
.GroupBy(p => p.Manifest.ContentPackFor.UniqueID, StringComparer.InvariantCultureIgnoreCase)
.ToDictionary(
group => group.Key,
group => group.Select(metadata => metadata.ContentPack).ToArray(),
StringComparer.InvariantCultureIgnoreCase
);
// load mods from metadata
using (AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.Platform, this.Monitor))
HashSet<string> suppressUpdateChecks = new HashSet<string>(this.Settings.SuppressUpdateChecks, StringComparer.InvariantCultureIgnoreCase);
InterfaceProxyFactory proxyFactory = new InterfaceProxyFactory();
foreach (IModMetadata mod in mods)
{
InterfaceProxyFactory proxyFactory = new InterfaceProxyFactory();
foreach (IModMetadata metadata in mods.Where(p => !p.IsContentPack))
if (!this.TryLoadMod(mod, modAssemblyLoader, proxyFactory, jsonHelper, contentCore, modDatabase, suppressUpdateChecks, out string errorPhrase, out string errorDetails))
{
// get basic info
IManifest manifest = metadata.Manifest;
this.Monitor.Log(metadata.Manifest?.EntryDll != null
? $" {metadata.DisplayName} ({PathUtilities.GetRelativePath(this.ModsPath, metadata.DirectoryPath)}{Path.DirectorySeparatorChar}{metadata.Manifest.EntryDll})..." // don't use Path.Combine here, since EntryDLL might not be valid
: $" {metadata.DisplayName}...", LogLevel.Trace);
// show warnings
if (metadata.HasManifest() && !metadata.HasValidUpdateKeys() && !suppressUpdateChecks.Contains(metadata.Manifest.UniqueID))
metadata.SetWarning(ModWarning.NoUpdateKeys);
// validate status
if (metadata.Status == ModMetadataStatus.Failed)
{
this.Monitor.Log($" Failed: {metadata.Error}", LogLevel.Trace);
TrackSkip(metadata, metadata.Error);
continue;
}
// load mod
string assemblyPath = metadata.Manifest?.EntryDll != null
? Path.Combine(metadata.DirectoryPath, metadata.Manifest.EntryDll)
: null;
Assembly modAssembly;
try
{
modAssembly = modAssemblyLoader.Load(metadata, assemblyPath, assumeCompatible: metadata.DataRecord?.Status == ModStatus.AssumeCompatible);
}
catch (IncompatibleInstructionException) // details already in trace logs
{
string[] updateUrls = new[] { modDatabase.GetModPageUrlFor(metadata.Manifest.UniqueID), "https://smapi.io/compat" }.Where(p => p != null).ToArray();
TrackSkip(metadata, $"it's no longer compatible. Please check for a new version at {string.Join(" or ", updateUrls)}.");
continue;
}
catch (SAssemblyLoadFailedException ex)
{
TrackSkip(metadata, $"it DLL couldn't be loaded: {ex.Message}");
continue;
}
catch (Exception ex)
{
TrackSkip(metadata, "its DLL couldn't be loaded.", $"Error: {ex.GetLogSummary()}");
continue;
}
// initialise mod
try
{
// get mod instance
if (!this.TryLoadModEntry(modAssembly, error => TrackSkip(metadata, error), out Mod mod))
continue;
// get content packs
if (!contentPacksByModID.TryGetValue(manifest.UniqueID, out IContentPack[] contentPacks))
contentPacks = new IContentPack[0];
// init mod helpers
IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName);
IModHelper modHelper;
{
IModEvents events = new ModEvents(metadata, this.EventManager);
ICommandHelper commandHelper = new CommandHelper(manifest.UniqueID, metadata.DisplayName, this.GameInstance.CommandManager);
IContentHelper contentHelper = new ContentHelper(contentCore, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor);
IDataHelper dataHelper = new DataHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper);
IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, metadata.DisplayName, this.Reflection, this.DeprecationManager);
IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry, proxyFactory, monitor);
IMultiplayerHelper multiplayerHelper = new MultiplayerHelper(manifest.UniqueID, this.GameInstance.Multiplayer);
ITranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentCore.GetLocale(), contentCore.Language);
IContentPack CreateTransitionalContentPack(string packDirPath, IManifest packManifest)
{
IMonitor packMonitor = this.GetSecondaryMonitor(packManifest.Name);
IContentHelper packContentHelper = new ContentHelper(contentCore, packDirPath, packManifest.UniqueID, packManifest.Name, packMonitor);
return new ContentPack(packDirPath, packManifest, packContentHelper, this.Toolkit.JsonHelper);
}
modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, this.Toolkit.JsonHelper, this.GameInstance.Input, events, contentHelper, commandHelper, dataHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper, contentPacks, CreateTransitionalContentPack, this.DeprecationManager);
}
// init mod
mod.ModManifest = manifest;
mod.Helper = modHelper;
mod.Monitor = monitor;
// track mod
metadata.SetMod(mod);
this.ModRegistry.Add(metadata);
}
catch (Exception ex)
{
TrackSkip(metadata, $"initialisation failed:\n{ex.GetLogSummary()}");
}
skippedMods[mod] = Tuple.Create(errorPhrase, errorDetails);
if (mod.Status != ModMetadataStatus.Failed)
mod.SetStatus(ModMetadataStatus.Failed, errorPhrase);
}
}
}
IModMetadata[] loadedContentPacks = this.ModRegistry.GetAll(assemblyMods: false).ToArray();
IModMetadata[] loadedMods = this.ModRegistry.GetAll(contentPacks: false).ToArray();
// log loaded mods
@ -964,11 +837,150 @@ namespace StardewModdingAPI.Framework
// unlock mod integrations
this.ModRegistry.AreAllModsInitialised = true;
}
/// <summary>Load a given mod.</summary>
/// <param name="mod">The mod to load.</param>
/// <param name="assemblyLoader">Preprocesses and loads mod assemblies</param>
/// <param name="proxyFactory">Generates proxy classes to access mod APIs through an arbitrary interface.</param>
/// <param name="jsonHelper">The JSON helper with which to read mods' JSON files.</param>
/// <param name="contentCore">The content manager to use for mod content.</param>
/// <param name="modDatabase">Handles access to SMAPI's internal mod metadata list.</param>
/// <param name="suppressUpdateChecks">The mod IDs to ignore when validating update keys.</param>
/// <param name="errorReasonPhrase">The user-facing reason phrase explaining why the mod couldn't be loaded (if applicable).</param>
/// <param name="errorDetails">More detailed details about the error intended for developers (if any).</param>
/// <returns>Returns whether the mod was successfully loaded.</returns>
private bool TryLoadMod(IModMetadata mod, AssemblyLoader assemblyLoader, InterfaceProxyFactory proxyFactory, JsonHelper jsonHelper, ContentCoordinator contentCore, ModDatabase modDatabase, HashSet<string> suppressUpdateChecks, out string errorReasonPhrase, out string errorDetails)
{
errorDetails = null;
// log entry
if (mod.IsContentPack)
this.Monitor.Log($" {mod.DisplayName} (content pack, {PathUtilities.GetRelativePath(this.ModsPath, mod.DirectoryPath)})...", LogLevel.Trace);
else
{
this.Monitor.Log(mod.Manifest?.EntryDll != null
? $" {mod.DisplayName} ({PathUtilities.GetRelativePath(this.ModsPath, mod.DirectoryPath)}{Path.DirectorySeparatorChar}{mod.Manifest.EntryDll})..." // don't use Path.Combine here, since EntryDLL might not be valid
: $" {mod.DisplayName}...", LogLevel.Trace);
}
// add warning for missing update key
if (mod.HasID() && !suppressUpdateChecks.Contains(mod.Manifest.UniqueID) && !mod.HasValidUpdateKeys())
mod.SetWarning(ModWarning.NoUpdateKeys);
// validate status
if (mod.Status == ModMetadataStatus.Failed)
{
this.Monitor.Log($" Failed: {mod.Error}", LogLevel.Trace);
errorReasonPhrase = mod.Error;
return false;
}
// load as content pack
if (mod.IsContentPack)
{
IManifest manifest = mod.Manifest;
IMonitor monitor = this.GetSecondaryMonitor(mod.DisplayName);
IContentHelper contentHelper = new ContentHelper(this.ContentCore, mod.DirectoryPath, manifest.UniqueID, mod.DisplayName, monitor);
IContentPack contentPack = new ContentPack(mod.DirectoryPath, manifest, contentHelper, jsonHelper);
mod.SetMod(contentPack, monitor);
this.ModRegistry.Add(mod);
errorReasonPhrase = null;
return true;
}
// load as mod
else
{
IManifest manifest = mod.Manifest;
// load mod
string assemblyPath = manifest?.EntryDll != null
? Path.Combine(mod.DirectoryPath, manifest.EntryDll)
: null;
Assembly modAssembly;
try
{
modAssembly = assemblyLoader.Load(mod, assemblyPath, assumeCompatible: mod.DataRecord?.Status == ModStatus.AssumeCompatible);
}
catch (IncompatibleInstructionException) // details already in trace logs
{
string[] updateUrls = new[] { modDatabase.GetModPageUrlFor(manifest.UniqueID), "https://smapi.io/compat" }.Where(p => p != null).ToArray();
errorReasonPhrase = $"it's no longer compatible. Please check for a new version at {string.Join(" or ", updateUrls)}.";
return false;
}
catch (SAssemblyLoadFailedException ex)
{
errorReasonPhrase = $"it DLL couldn't be loaded: {ex.Message}";
return false;
}
catch (Exception ex)
{
errorReasonPhrase = "its DLL couldn't be loaded.";
errorDetails = $"Error: {ex.GetLogSummary()}";
return false;
}
// initialise mod
try
{
// get mod instance
if (!this.TryLoadModEntry(modAssembly, out Mod modEntry, out errorReasonPhrase))
return false;
// get content packs
IContentPack[] contentPacks = this.ModRegistry
.GetAll(assemblyMods: false)
.Where(p => p.IsContentPack && mod.Manifest.UniqueID.Equals(p.Manifest.ContentPackFor.UniqueID, StringComparison.InvariantCultureIgnoreCase))
.Select(p => p.ContentPack)
.ToArray();
// init mod helpers
IMonitor monitor = this.GetSecondaryMonitor(mod.DisplayName);
IModHelper modHelper;
{
IModEvents events = new ModEvents(mod, this.EventManager);
ICommandHelper commandHelper = new CommandHelper(manifest.UniqueID, mod.DisplayName, this.GameInstance.CommandManager);
IContentHelper contentHelper = new ContentHelper(contentCore, mod.DirectoryPath, manifest.UniqueID, mod.DisplayName, monitor);
IDataHelper dataHelper = new DataHelper(manifest.UniqueID, mod.DirectoryPath, jsonHelper);
IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, mod.DisplayName, this.Reflection, this.DeprecationManager);
IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry, proxyFactory, monitor);
IMultiplayerHelper multiplayerHelper = new MultiplayerHelper(manifest.UniqueID, this.GameInstance.Multiplayer);
ITranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentCore.GetLocale(), contentCore.Language);
IContentPack CreateTransitionalContentPack(string packDirPath, IManifest packManifest)
{
IMonitor packMonitor = this.GetSecondaryMonitor(packManifest.Name);
IContentHelper packContentHelper = new ContentHelper(contentCore, packDirPath, packManifest.UniqueID, packManifest.Name, packMonitor);
return new ContentPack(packDirPath, packManifest, packContentHelper, this.Toolkit.JsonHelper);
}
modHelper = new ModHelper(manifest.UniqueID, mod.DirectoryPath, this.Toolkit.JsonHelper, this.GameInstance.Input, events, contentHelper, commandHelper, dataHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper, contentPacks, CreateTransitionalContentPack, this.DeprecationManager);
}
// init mod
modEntry.ModManifest = manifest;
modEntry.Helper = modHelper;
modEntry.Monitor = monitor;
// track mod
mod.SetMod(modEntry);
this.ModRegistry.Add(mod);
return true;
}
catch (Exception ex)
{
errorReasonPhrase = $"initialisation failed:\n{ex.GetLogSummary()}";
return false;
}
}
}
/// <summary>Write a summary of mod warnings to the console and log.</summary>
/// <param name="mods">The loaded mods.</param>
/// <param name="skippedMods">The mods which were skipped, along with the friendly and developer reasons.</param>
private void LogModWarnings(IModMetadata[] mods, IDictionary<IModMetadata, string[]> skippedMods)
private void LogModWarnings(IModMetadata[] mods, IDictionary<IModMetadata, Tuple<string, string>> skippedMods)
{
// get mods with warnings
IModMetadata[] modsWithWarnings = mods.Where(p => p.Warnings != ModWarning.None).ToArray();
@ -992,11 +1004,12 @@ namespace StardewModdingAPI.Framework
foreach (var pair in skippedMods.OrderBy(p => p.Key.DisplayName))
{
IModMetadata mod = pair.Key;
string[] reason = pair.Value;
string errorReason = pair.Value.Item1;
string errorDetails = pair.Value.Item2;
this.Monitor.Log($" - {mod.DisplayName}{(mod.Manifest?.Version != null ? " " + mod.Manifest.Version.ToString() : "")} because {reason[0]}", LogLevel.Error);
if (reason[1] != null)
this.Monitor.Log($" ({reason[1]})", LogLevel.Trace);
this.Monitor.Log($" - {mod.DisplayName}{(mod.Manifest?.Version != null ? " " + mod.Manifest.Version.ToString() : "")} because {errorReason}", LogLevel.Error);
if (errorDetails != null)
this.Monitor.Log($" ({errorDetails})", LogLevel.Trace);
}
this.Monitor.Newline();
}
@ -1061,9 +1074,10 @@ namespace StardewModdingAPI.Framework
/// <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)
/// <param name="error">The error indicating why loading failed (if applicable).</param>
/// <returns>Returns whether the mod entry class was successfully loaded.</returns>
private bool TryLoadModEntry(Assembly modAssembly, out Mod mod, out string error)
{
mod = null;
@ -1071,12 +1085,12 @@ namespace StardewModdingAPI.Framework
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.");
error = $"its DLL has no '{nameof(Mod)}' subclass.";
return false;
}
if (modEntries.Length > 1)
{
onError($"its DLL contains multiple '{nameof(Mod)}' subclasses.");
error = $"its DLL contains multiple '{nameof(Mod)}' subclasses.";
return false;
}
@ -1084,10 +1098,11 @@ namespace StardewModdingAPI.Framework
mod = (Mod)modAssembly.CreateInstance(modEntries[0].ToString());
if (mod == null)
{
onError("its entry class couldn't be instantiated.");
error = "its entry class couldn't be instantiated.";
return false;
}
error = null;
return true;
}