summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJesse Plamondon-Willard <github@jplamondonw.com>2017-05-11 22:25:45 -0400
committerJesse Plamondon-Willard <github@jplamondonw.com>2017-05-11 22:25:45 -0400
commit03876153f4fbb13b8a260c529513a306319f9e05 (patch)
tree6913e9c42e792b2a36c9d3dd23249932d10c8a35 /src
parent48c5c9e36794ed3dae0cf6114194b0dc80dd4725 (diff)
downloadSMAPI-03876153f4fbb13b8a260c529513a306319f9e05.tar.gz
SMAPI-03876153f4fbb13b8a260c529513a306319f9e05.tar.bz2
SMAPI-03876153f4fbb13b8a260c529513a306319f9e05.zip
decouple mod metadata vs assembly loading to enable upcoming mod dependencies (#285)
Diffstat (limited to 'src')
-rw-r--r--src/StardewModdingAPI/Framework/ModMetadata.cs40
-rw-r--r--src/StardewModdingAPI/Program.cs139
-rw-r--r--src/StardewModdingAPI/StardewModdingAPI.csproj1
3 files changed, 124 insertions, 56 deletions
diff --git a/src/StardewModdingAPI/Framework/ModMetadata.cs b/src/StardewModdingAPI/Framework/ModMetadata.cs
new file mode 100644
index 00000000..aeb9261a
--- /dev/null
+++ b/src/StardewModdingAPI/Framework/ModMetadata.cs
@@ -0,0 +1,40 @@
+using StardewModdingAPI.Framework.Models;
+
+namespace StardewModdingAPI.Framework
+{
+ /// <summary>Metadata for a mod.</summary>
+ internal class ModMetadata
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The mod's display name.</summary>
+ public string DisplayName { get; }
+
+ /// <summary>The mod's full directory path.</summary>
+ public string DirectoryPath { get; }
+
+ /// <summary>The mod manifest.</summary>
+ public IManifest Manifest { get; }
+
+ /// <summary>Optional metadata about a mod version that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code.</summary>
+ public ModCompatibility Compatibility { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="displayName">The mod's display name.</param>
+ /// <param name="directoryPath">The mod's full directory path.</param>
+ /// <param name="manifest">The mod manifest.</param>
+ /// <param name="compatibility">Optional metadata about a mod version that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code.</param>
+ public ModMetadata(string displayName, string directoryPath, IManifest manifest, ModCompatibility compatibility)
+ {
+ this.DisplayName = displayName;
+ this.DirectoryPath = directoryPath;
+ this.Manifest = manifest;
+ this.Compatibility = compatibility;
+ }
+ }
+}
diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs
index 70b2fbc1..a5bd7788 100644
--- a/src/StardewModdingAPI/Program.cs
+++ b/src/StardewModdingAPI/Program.cs
@@ -310,7 +310,18 @@ namespace StardewModdingAPI
this.Monitor.Log("SMAPI found problems in the game's XNB files which may cause errors or crashes while you're playing. Consider uninstalling XNB mods or reinstalling the game.", LogLevel.Warn);
// load mods
- int modsLoaded = this.LoadMods();
+ int modsLoaded;
+ {
+ // load mods
+ JsonHelper jsonHelper = new JsonHelper();
+ IList<Action> deprecationWarnings = new List<Action>();
+ ModMetadata[] mods = this.FindMods(Constants.ModPath, new JsonHelper(), deprecationWarnings);
+ modsLoaded = this.LoadMods(mods, jsonHelper, (SContentManager)Game1.content, deprecationWarnings);
+
+ // log deprecation warnings together
+ foreach (Action warning in deprecationWarnings)
+ warning();
+ }
if (this.Monitor.IsExiting)
{
this.Monitor.Log("SMAPI shutting down: aborting initialisation.", LogLevel.Warn);
@@ -444,32 +455,27 @@ namespace StardewModdingAPI
}
}
- /// <summary>Load and hook up all mods in the mod directory.</summary>
- /// <returns>Returns the number of mods loaded.</returns>
- private int LoadMods()
+ /// <summary>Find all mods in the given folder.</summary>
+ /// <param name="rootPath">The root mod path to search.</param>
+ /// <param name="jsonHelper">The JSON helper with which to read the manifest file.</param>
+ /// <param name="deprecationWarnings">A list to populate with any deprecation warnings.</param>
+ private ModMetadata[] FindMods(string rootPath, JsonHelper jsonHelper, IList<Action> deprecationWarnings)
{
- this.Monitor.Log("Loading mods...");
-
- // get JSON helper
- JsonHelper jsonHelper = new JsonHelper();
-
- // get assembly loader
- AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.TargetPlatform, this.Monitor);
- AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => modAssemblyLoader.ResolveAssembly(e.Name);
+ this.Monitor.Log("Finding mods...");
+ void LogSkip(string displayName, string reasonPhrase, LogLevel level = LogLevel.Error) => this.Monitor.Log($"Skipped {displayName} because {reasonPhrase}", level);
- // load mod assemblies
- int modsLoaded = 0;
- List<Action> deprecationWarnings = new List<Action>(); // queue up deprecation warnings to show after mod list
- foreach (string directoryPath in Directory.GetDirectories(Constants.ModPath))
+ // load mod metadata
+ List<ModMetadata> mods = new List<ModMetadata>();
+ foreach (string modRootPath in Directory.GetDirectories(rootPath))
{
if (this.Monitor.IsExiting)
- {
- this.Monitor.Log("SMAPI shutting down: aborting mod scan.", LogLevel.Warn);
- return modsLoaded;
- }
+ return new ModMetadata[0]; // exit in progress
+
+ // init metadata
+ string displayName = modRootPath.Replace(rootPath, "").Trim('/', '\\');
// passthrough empty directories
- DirectoryInfo directory = new DirectoryInfo(directoryPath);
+ DirectoryInfo directory = new DirectoryInfo(modRootPath);
while (!directory.GetFiles().Any() && directory.GetDirectories().Length == 1)
directory = directory.GetDirectories().First();
@@ -477,35 +483,34 @@ namespace StardewModdingAPI
string manifestPath = Path.Combine(directory.FullName, "manifest.json");
if (!File.Exists(manifestPath))
{
- this.Monitor.Log($"Ignored folder \"{directory.Name}\" which doesn't have a manifest.json.", LogLevel.Warn);
+ LogSkip(displayName, "it doesn't have a manifest.", LogLevel.Warn);
continue;
}
- string skippedPrefix = $"Skipped {manifestPath.Replace(Constants.ModPath, "").Trim('/', '\\')}";
// read manifest
Manifest manifest;
try
{
- // read manifest text
+ // read manifest file
string json = File.ReadAllText(manifestPath);
if (string.IsNullOrEmpty(json))
{
- this.Monitor.Log($"{skippedPrefix} because the manifest is empty.", LogLevel.Error);
+ LogSkip(displayName, "its manifest is empty.");
continue;
}
- // deserialise manifest
+ // parse manifest
manifest = jsonHelper.ReadJsonFile<Manifest>(Path.Combine(directory.FullName, "manifest.json"));
if (manifest == null)
{
- this.Monitor.Log($"{skippedPrefix} because its manifest is invalid.", LogLevel.Error);
+ LogSkip(displayName, "its manifest is invalid.");
continue;
}
// validate manifest
if (string.IsNullOrWhiteSpace(manifest.EntryDll))
{
- this.Monitor.Log($"{skippedPrefix} because its manifest doesn't specify an entry DLL.", LogLevel.Error);
+ LogSkip(displayName, "its manifest doesn't set an entry DLL.");
continue;
}
if (string.IsNullOrWhiteSpace(manifest.UniqueID))
@@ -513,11 +518,11 @@ namespace StardewModdingAPI
}
catch (Exception ex)
{
- this.Monitor.Log($"{skippedPrefix} because manifest parsing failed.\n{ex.GetLogSummary()}", LogLevel.Error);
+ LogSkip(displayName, $"parsing its manifest failed:\n{ex.GetLogSummary()}");
continue;
}
if (!string.IsNullOrWhiteSpace(manifest.Name))
- skippedPrefix = $"Skipped {manifest.Name}";
+ displayName = manifest.Name;
// validate compatibility
ModCompatibility compatibility = this.ModRegistry.GetCompatibilityRecord(manifest);
@@ -527,14 +532,13 @@ namespace StardewModdingAPI
bool hasUnofficialUrl = !string.IsNullOrWhiteSpace(compatibility.UnofficialUpdateUrl);
string reasonPhrase = compatibility.ReasonPhrase ?? "it's not compatible with the latest version of the game";
- string warning = $"{skippedPrefix} because {reasonPhrase}. Please check for a version newer than {compatibility.UpperVersion} here:";
+ string error = $"{reasonPhrase}. Please check for a version newer than {compatibility.UpperVersion} here:";
if (hasOfficialUrl)
- warning += !hasUnofficialUrl ? $" {compatibility.UpdateUrl}" : $"{Environment.NewLine}- official mod: {compatibility.UpdateUrl}";
+ error += !hasUnofficialUrl ? $" {compatibility.UpdateUrl}" : $"{Environment.NewLine}- official mod: {compatibility.UpdateUrl}";
if (hasUnofficialUrl)
- warning += $"{Environment.NewLine}- unofficial update: {compatibility.UnofficialUpdateUrl}";
+ error += $"{Environment.NewLine}- unofficial update: {compatibility.UnofficialUpdateUrl}";
- this.Monitor.Log(warning, LogLevel.Error);
- continue;
+ LogSkip(displayName, error);
}
// validate SMAPI version
@@ -545,13 +549,13 @@ namespace StardewModdingAPI
ISemanticVersion minVersion = new SemanticVersion(manifest.MinimumApiVersion);
if (minVersion.IsNewerThan(Constants.ApiVersion))
{
- this.Monitor.Log($"{skippedPrefix} because it needs SMAPI {minVersion} or later. Please update SMAPI to the latest version to use this mod.", LogLevel.Error);
+ LogSkip(displayName, $"it needs SMAPI {minVersion} or later. Please update SMAPI to the latest version to use this mod.");
continue;
}
}
catch (FormatException ex) when (ex.Message.Contains("not a valid semantic version"))
{
- this.Monitor.Log($"{skippedPrefix} because it has an invalid minimum SMAPI version '{manifest.MinimumApiVersion}'. This should be a semantic version number like {Constants.ApiVersion}.", LogLevel.Error);
+ LogSkip(displayName, $"it has an invalid minimum SMAPI version '{manifest.MinimumApiVersion}'. This should be a semantic version number like {Constants.ApiVersion}.");
continue;
}
}
@@ -566,39 +570,66 @@ namespace StardewModdingAPI
Directory.CreateDirectory(psDir);
if (!Directory.Exists(psDir))
{
- this.Monitor.Log($"{skippedPrefix} because it requires per-save configuration files ('psconfigs') which couldn't be created for some reason.", LogLevel.Error);
+ LogSkip(displayName, "it requires per-save configuration files ('psconfigs') which couldn't be created for some reason.");
continue;
}
}
catch (Exception ex)
{
- this.Monitor.Log($"{skippedPrefix} because it requires per-save configuration files ('psconfigs') which couldn't be created:\n{ex.GetLogSummary()}", LogLevel.Error);
+ LogSkip(displayName, $"it requires per-save configuration files ('psconfigs') which couldn't be created: {ex.GetLogSummary()}");
continue;
}
}
- // validate mod path to simplify errors
+ // validate DLL path
string assemblyPath = Path.Combine(directory.FullName, manifest.EntryDll);
if (!File.Exists(assemblyPath))
{
- this.Monitor.Log($"{skippedPrefix} because its DLL '{manifest.EntryDll}' doesn't exist.", LogLevel.Error);
+ LogSkip(displayName, $"its DLL '{manifest.EntryDll}' doesn't exist.");
continue;
}
+ // add mod metadata
+ mods.Add(new ModMetadata(displayName, directory.FullName, manifest, compatibility));
+ }
+
+ return mods.ToArray();
+ }
+
+ /// <summary>Load and hook up the given mods.</summary>
+ /// <param name="mods">The mods to load.</param>
+ /// <param name="jsonHelper">The JSON helper with which to read mods' JSON files.</param>
+ /// <param name="contentManager">The content manager to use for mod content.</param>
+ /// <param name="deprecationWarnings">A list to populate with any deprecation warnings.</param>
+ /// <returns>Returns the number of mods successfully loaded.</returns>
+ private int LoadMods(ModMetadata[] mods, JsonHelper jsonHelper, SContentManager contentManager, IList<Action> deprecationWarnings)
+ {
+ this.Monitor.Log("Loading mods...");
+ void LogSkip(ModMetadata mod, string reasonPhrase, LogLevel level = LogLevel.Error) => this.Monitor.Log($"Skipped {mod.DisplayName} because {reasonPhrase}", level);
+
+ // load mod assemblies
+ int modsLoaded = 0;
+ AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.TargetPlatform, this.Monitor);
+ AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => modAssemblyLoader.ResolveAssembly(e.Name);
+ foreach (ModMetadata metadata in mods)
+ {
+ IManifest manifest = metadata.Manifest;
+ string assemblyPath = Path.Combine(metadata.DirectoryPath, metadata.Manifest.EntryDll);
+
// preprocess & load mod assembly
Assembly modAssembly;
try
{
- modAssembly = modAssemblyLoader.Load(assemblyPath, assumeCompatible: compatibility?.Compatibility == ModCompatibilityType.AssumeCompatible);
+ modAssembly = modAssemblyLoader.Load(assemblyPath, assumeCompatible: metadata.Compatibility?.Compatibility == ModCompatibilityType.AssumeCompatible);
}
catch (IncompatibleInstructionException ex)
{
- this.Monitor.Log($"{skippedPrefix} because it's not compatible with the latest version of the game (detected {ex.NounPhrase}). Please check for a newer version of the mod (you have v{manifest.Version}).", LogLevel.Error);
+ LogSkip(metadata, $"it's not compatible with the latest version of the game (detected {ex.NounPhrase}). Please check for a newer version of the mod (you have v{manifest.Version}).");
continue;
}
catch (Exception ex)
{
- this.Monitor.Log($"{skippedPrefix} because its DLL '{manifest.EntryDll}' couldn't be loaded.\n{ex.GetLogSummary()}", LogLevel.Error);
+ LogSkip(metadata, $"its DLL '{manifest.EntryDll}' couldn't be loaded:\n{ex.GetLogSummary()}");
continue;
}
@@ -608,18 +639,18 @@ namespace StardewModdingAPI
int modEntries = modAssembly.DefinedTypes.Count(type => typeof(Mod).IsAssignableFrom(type) && !type.IsAbstract);
if (modEntries == 0)
{
- this.Monitor.Log($"{skippedPrefix} because its DLL has no '{nameof(Mod)}' subclass.", LogLevel.Error);
+ LogSkip(metadata, $"its DLL has no '{nameof(Mod)}' subclass.");
continue;
}
if (modEntries > 1)
{
- this.Monitor.Log($"{skippedPrefix} because its DLL contains multiple '{nameof(Mod)}' subclasses.", LogLevel.Error);
+ LogSkip(metadata, $"its DLL contains multiple '{nameof(Mod)}' subclasses.");
continue;
}
}
catch (Exception ex)
{
- this.Monitor.Log($"{skippedPrefix} because its DLL couldn't be loaded.\n{ex.GetLogSummary()}", LogLevel.Error);
+ LogSkip(metadata, $"its DLL couldn't be loaded:\n{ex.GetLogSummary()}");
continue;
}
@@ -631,16 +662,15 @@ namespace StardewModdingAPI
Mod mod = (Mod)modAssembly.CreateInstance(modEntryType.ToString());
if (mod == null)
{
- this.Monitor.Log($"{skippedPrefix} because its entry class couldn't be instantiated.");
+ LogSkip(metadata, "its entry class couldn't be instantiated.");
continue;
}
// inject data
- // get helper
mod.ModManifest = manifest;
- mod.Helper = new ModHelper(manifest, directory.FullName, jsonHelper, this.ModRegistry, this.CommandManager, (SContentManager)Game1.content, this.Reflection);
+ mod.Helper = new ModHelper(manifest, metadata.DirectoryPath, jsonHelper, this.ModRegistry, this.CommandManager, contentManager, this.Reflection);
mod.Monitor = this.GetSecondaryMonitor(manifest.Name);
- mod.PathOnDisk = directory.FullName;
+ mod.PathOnDisk = metadata.DirectoryPath;
// track mod
this.ModRegistry.Add(mod);
@@ -649,11 +679,11 @@ namespace StardewModdingAPI
}
catch (Exception ex)
{
- this.Monitor.Log($"{skippedPrefix} because initialisation failed:\n{ex.GetLogSummary()}", LogLevel.Error);
+ LogSkip(metadata, $"initialisation failed:\n{ex.GetLogSummary()}");
}
}
- // initialise mods
+ // initialise loaded mods
foreach (IMod mod in this.ModRegistry.GetMods())
{
try
@@ -674,9 +704,6 @@ namespace StardewModdingAPI
// print result
this.Monitor.Log($"Loaded {modsLoaded} mods.");
- foreach (Action warning in deprecationWarnings)
- warning();
-
return modsLoaded;
}
diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj
index 87ce65b0..69c167c9 100644
--- a/src/StardewModdingAPI/StardewModdingAPI.csproj
+++ b/src/StardewModdingAPI/StardewModdingAPI.csproj
@@ -158,6 +158,7 @@
<Compile Include="Framework\CommandHelper.cs" />
<Compile Include="Framework\Models\ModCompatibilityType.cs" />
<Compile Include="Framework\Models\SConfig.cs" />
+ <Compile Include="Framework\ModMetadata.cs" />
<Compile Include="Framework\Reflection\PrivateProperty.cs" />
<Compile Include="Framework\RequestExitDelegate.cs" />
<Compile Include="Framework\SContentManager.cs" />