diff --git a/src/SMAPI.Tests/Core/ModResolverTests.cs b/src/SMAPI.Tests/Core/ModResolverTests.cs
index 9e91b993..a38621f8 100644
--- a/src/SMAPI.Tests/Core/ModResolverTests.cs
+++ b/src/SMAPI.Tests/Core/ModResolverTests.cs
@@ -7,8 +7,8 @@ using Newtonsoft.Json;
using NUnit.Framework;
using StardewModdingAPI.Framework;
using StardewModdingAPI.Framework.ModLoading;
+using StardewModdingAPI.Toolkit;
using StardewModdingAPI.Toolkit.Framework.ModData;
-using StardewModdingAPI.Toolkit.Serialisation;
using StardewModdingAPI.Toolkit.Serialisation.Models;
namespace StardewModdingAPI.Tests.Core
@@ -31,7 +31,7 @@ namespace StardewModdingAPI.Tests.Core
Directory.CreateDirectory(rootFolder);
// act
- IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModDatabase()).ToArray();
+ IModMetadata[] mods = new ModResolver().ReadManifests(new ModToolkit(), rootFolder, new ModDatabase()).ToArray();
// assert
Assert.AreEqual(0, mods.Length, 0, $"Expected to find zero manifests, found {mods.Length} instead.");
@@ -46,7 +46,7 @@ namespace StardewModdingAPI.Tests.Core
Directory.CreateDirectory(modFolder);
// act
- IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModDatabase()).ToArray();
+ IModMetadata[] mods = new ModResolver().ReadManifests(new ModToolkit(), rootFolder, new ModDatabase()).ToArray();
IModMetadata mod = mods.FirstOrDefault();
// assert
@@ -85,7 +85,7 @@ namespace StardewModdingAPI.Tests.Core
File.WriteAllText(filename, JsonConvert.SerializeObject(original));
// act
- IModMetadata[] mods = new ModResolver().ReadManifests(rootFolder, new JsonHelper(), new ModDatabase()).ToArray();
+ IModMetadata[] mods = new ModResolver().ReadManifests(new ModToolkit(), rootFolder, new ModDatabase()).ToArray();
IModMetadata mod = mods.FirstOrDefault();
// assert
diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs
index 174820a1..9ac95fd4 100644
--- a/src/SMAPI/Framework/ModLoading/ModResolver.cs
+++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs
@@ -3,8 +3,9 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
+using StardewModdingAPI.Toolkit;
using StardewModdingAPI.Toolkit.Framework.ModData;
-using StardewModdingAPI.Toolkit.Serialisation;
+using StardewModdingAPI.Toolkit.Framework.ModScanning;
using StardewModdingAPI.Toolkit.Serialisation.Models;
using StardewModdingAPI.Toolkit.Utilities;
@@ -17,38 +18,15 @@ namespace StardewModdingAPI.Framework.ModLoading
** Public methods
*********/
/// Get manifest metadata for each folder in the given root path.
+ /// The mod toolkit.
/// The root path to search for mods.
- /// The JSON helper with which to read manifests.
/// Handles access to SMAPI's internal mod metadata list.
/// Returns the manifests by relative folder.
- public IEnumerable ReadManifests(string rootPath, JsonHelper jsonHelper, ModDatabase modDatabase)
+ public IEnumerable ReadManifests(ModToolkit toolkit, string rootPath, ModDatabase modDatabase)
{
- foreach (DirectoryInfo modDir in this.GetModFolders(rootPath))
+ foreach (ModFolder folder in toolkit.GetModFolders(rootPath))
{
- // read file
- Manifest manifest = null;
- string error = null;
- {
- string path = Path.Combine(modDir.FullName, "manifest.json");
- try
- {
- manifest = jsonHelper.ReadJsonFile(path);
- if (manifest == null)
- {
- error = File.Exists(path)
- ? "its manifest is invalid."
- : "it doesn't have a manifest.";
- }
- }
- catch (SParseException ex)
- {
- error = $"parsing its manifest failed: {ex.Message}";
- }
- catch (Exception ex)
- {
- error = $"parsing its manifest failed:\n{ex.GetLogSummary()}";
- }
- }
+ Manifest manifest = folder.Manifest;
// parse internal data record (if any)
ModDataRecordVersionedFields dataRecord = modDatabase.Get(manifest?.UniqueID)?.GetVersionedFields(manifest);
@@ -58,7 +36,7 @@ namespace StardewModdingAPI.Framework.ModLoading
if (string.IsNullOrWhiteSpace(displayName))
displayName = dataRecord?.DisplayName;
if (string.IsNullOrWhiteSpace(displayName))
- displayName = PathUtilities.GetRelativePath(rootPath, modDir.FullName);
+ displayName = PathUtilities.GetRelativePath(rootPath, folder.ActualDirectory?.FullName ?? folder.SearchDirectory.FullName);
// apply defaults
if (manifest != null && dataRecord != null)
@@ -68,10 +46,10 @@ namespace StardewModdingAPI.Framework.ModLoading
}
// build metadata
- ModMetadataStatus status = error == null
+ ModMetadataStatus status = folder.ManifestParseError == null
? ModMetadataStatus.Found
: ModMetadataStatus.Failed;
- yield return new ModMetadata(displayName, modDir.FullName, manifest, dataRecord).SetStatus(status, error);
+ yield return new ModMetadata(displayName, folder.ActualDirectory?.FullName, manifest, dataRecord).SetStatus(status, folder.ManifestParseError);
}
}
diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs
index a88db105..c9266c69 100644
--- a/src/SMAPI/Program.cs
+++ b/src/SMAPI/Program.cs
@@ -104,8 +104,8 @@ namespace StardewModdingAPI
new Regex(@"^DebugOutput: (?:added CLOUD|dismount tile|Ping|playerPos)", RegexOptions.Compiled | RegexOptions.CultureInvariant)
};
- /// Encapsulates SMAPI's JSON file parsing.
- private readonly JsonHelper JsonHelper = new JsonHelper();
+ /// The mod toolkit used for generic mod interactions.
+ private readonly ModToolkit Toolkit = new ModToolkit();
/*********
@@ -205,7 +205,7 @@ namespace StardewModdingAPI
new RectangleConverter()
};
foreach (JsonConverter converter in converters)
- this.JsonHelper.JsonSettings.Converters.Add(converter);
+ this.Toolkit.JsonHelper.JsonSettings.Converters.Add(converter);
// add error handlers
#if SMAPI_FOR_WINDOWS
@@ -423,14 +423,14 @@ namespace StardewModdingAPI
ModResolver resolver = new ModResolver();
// load manifests
- IModMetadata[] mods = resolver.ReadManifests(Constants.ModPath, this.JsonHelper, modDatabase).ToArray();
+ IModMetadata[] mods = resolver.ReadManifests(toolkit, Constants.ModPath, modDatabase).ToArray();
resolver.ValidateManifests(mods, Constants.ApiVersion, toolkit.GetUpdateUrl);
// process dependencies
mods = resolver.ProcessDependencies(mods, modDatabase).ToArray();
// load mods
- this.LoadMods(mods, this.JsonHelper, this.ContentCore, modDatabase);
+ this.LoadMods(mods, this.Toolkit.JsonHelper, this.ContentCore, modDatabase);
// write metadata file
if (this.Settings.DumpMetadata)
@@ -443,7 +443,7 @@ namespace StardewModdingAPI
ModFolderPath = Constants.ModPath,
Mods = mods
};
- this.JsonHelper.WriteJsonFile(Path.Combine(Constants.LogDir, $"{Constants.LogNamePrefix}.metadata-dump.json"), export);
+ this.Toolkit.JsonHelper.WriteJsonFile(Path.Combine(Constants.LogDir, $"{Constants.LogNamePrefix}.metadata-dump.json"), export);
}
// check for updates
@@ -875,7 +875,7 @@ namespace StardewModdingAPI
{
IMonitor packMonitor = this.GetSecondaryMonitor(packManifest.Name);
IContentHelper packContentHelper = new ContentHelper(contentCore, packDirPath, packManifest.UniqueID, packManifest.Name, packMonitor);
- return new ContentPack(packDirPath, packManifest, packContentHelper, this.JsonHelper);
+ return new ContentPack(packDirPath, packManifest, packContentHelper, this.Toolkit.JsonHelper);
}
modHelper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, this.GameInstance.Input, events, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper, contentPacks, CreateTransitionalContentPack, this.DeprecationManager);
@@ -1117,7 +1117,7 @@ namespace StardewModdingAPI
/// The mods for which to reload translations.
private void ReloadTranslations(IEnumerable mods)
{
- JsonHelper jsonHelper = this.JsonHelper;
+ JsonHelper jsonHelper = this.Toolkit.JsonHelper;
foreach (IModMetadata metadata in mods)
{
if (metadata.IsContentPack)
diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModFolder.cs b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModFolder.cs
new file mode 100644
index 00000000..9b6853b4
--- /dev/null
+++ b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModFolder.cs
@@ -0,0 +1,60 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using StardewModdingAPI.Toolkit.Serialisation.Models;
+
+namespace StardewModdingAPI.Toolkit.Framework.ModScanning
+{
+ /// The info about a mod read from its folder.
+ public class ModFolder
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// The Mods subfolder containing this mod.
+ public DirectoryInfo SearchDirectory { get; }
+
+ /// The folder containing manifest.json.
+ public DirectoryInfo ActualDirectory { get; }
+
+ /// The mod manifest.
+ public Manifest Manifest { get; }
+
+ /// The error which occurred parsing the manifest, if any.
+ public string ManifestParseError { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance when a mod wasn't found in a folder.
+ /// The directory that was searched.
+ public ModFolder(DirectoryInfo searchDirectory)
+ {
+ this.SearchDirectory = searchDirectory;
+ }
+
+ /// Construct an instance.
+ /// The Mods subfolder containing this mod.
+ /// The folder containing manifest.json.
+ /// The mod manifest.
+ /// The error which occurred parsing the manifest, if any.
+ public ModFolder(DirectoryInfo searchDirectory, DirectoryInfo actualDirectory, Manifest manifest, string manifestParseError = null)
+ {
+ this.SearchDirectory = searchDirectory;
+ this.ActualDirectory = actualDirectory;
+ this.Manifest = manifest;
+ this.ManifestParseError = manifestParseError;
+ }
+
+ /// Get the update keys for a mod.
+ /// The mod manifest.
+ public IEnumerable GetUpdateKeys(Manifest manifest)
+ {
+ return
+ (manifest.UpdateKeys ?? new string[0])
+ .Where(p => !string.IsNullOrWhiteSpace(p))
+ .ToArray();
+ }
+ }
+}
diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs
new file mode 100644
index 00000000..d3662c9c
--- /dev/null
+++ b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs
@@ -0,0 +1,103 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using StardewModdingAPI.Toolkit.Serialisation;
+using StardewModdingAPI.Toolkit.Serialisation.Models;
+
+namespace StardewModdingAPI.Toolkit.Framework.ModScanning
+{
+ /// Scans folders for mod data.
+ public class ModScanner
+ {
+ /*********
+ ** Properties
+ *********/
+ /// The JSON helper with which to read manifests.
+ private readonly JsonHelper JsonHelper;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// The JSON helper with which to read manifests.
+ public ModScanner(JsonHelper jsonHelper)
+ {
+ this.JsonHelper = jsonHelper;
+ }
+
+ /// Extract information about all mods in the given folder.
+ /// The root folder containing mods.
+ public IEnumerable GetModFolders(string rootPath)
+ {
+ foreach (DirectoryInfo folder in new DirectoryInfo(rootPath).EnumerateDirectories())
+ yield return this.ReadFolder(rootPath, folder);
+ }
+
+ /// Extract information from a mod folder.
+ /// The root folder containing mods.
+ /// The folder to search for a mod.
+ public ModFolder ReadFolder(string rootPath, DirectoryInfo searchFolder)
+ {
+ // find manifest.json
+ FileInfo manifestFile = this.FindManifest(searchFolder);
+ if (manifestFile == null)
+ return new ModFolder(searchFolder);
+
+ // read mod info
+ Manifest manifest = null;
+ string manifestError = null;
+ {
+ try
+ {
+ manifest = this.JsonHelper.ReadJsonFile(manifestFile.FullName);
+ if (manifest == null)
+ {
+ manifestError = File.Exists(manifestFile.FullName)
+ ? "its manifest is invalid."
+ : "it doesn't have a manifest.";
+ }
+ }
+ catch (SParseException ex)
+ {
+ manifestError = $"parsing its manifest failed: {ex.Message}";
+ }
+ catch (Exception ex)
+ {
+ manifestError = $"parsing its manifest failed:\n{ex}";
+ }
+ }
+
+ return new ModFolder(searchFolder, manifestFile.Directory, manifest, manifestError);
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// Find the manifest for a mod folder.
+ /// The folder to search.
+ private FileInfo FindManifest(DirectoryInfo folder)
+ {
+ while (true)
+ {
+ // check for manifest in current folder
+ FileInfo file = new FileInfo(Path.Combine(folder.FullName, "manifest.json"));
+ if (file.Exists)
+ return file;
+
+ // check for single subfolder
+ FileSystemInfo[] entries = folder.EnumerateFileSystemInfos().Take(2).ToArray();
+ if (entries.Length == 1 && entries[0] is DirectoryInfo subfolder)
+ {
+ folder = subfolder;
+ continue;
+ }
+
+ // not found
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/StardewModdingAPI.Toolkit/ModToolkit.cs b/src/StardewModdingAPI.Toolkit/ModToolkit.cs
index 18fe1ff3..8c78b2f3 100644
--- a/src/StardewModdingAPI.Toolkit/ModToolkit.cs
+++ b/src/StardewModdingAPI.Toolkit/ModToolkit.cs
@@ -6,6 +6,8 @@ using System.Threading.Tasks;
using Newtonsoft.Json;
using StardewModdingAPI.Toolkit.Framework.Clients.Wiki;
using StardewModdingAPI.Toolkit.Framework.ModData;
+using StardewModdingAPI.Toolkit.Framework.ModScanning;
+using StardewModdingAPI.Toolkit.Serialisation;
namespace StardewModdingAPI.Toolkit
{
@@ -27,6 +29,13 @@ namespace StardewModdingAPI.Toolkit
};
+ /*********
+ ** Accessors
+ *********/
+ /// Encapsulates SMAPI's JSON parsing.
+ public JsonHelper JsonHelper { get; } = new JsonHelper();
+
+
/*********
** Public methods
*********/
@@ -53,6 +62,13 @@ namespace StardewModdingAPI.Toolkit
return new ModDatabase(records, this.GetUpdateUrl);
}
+ /// Extract information about all mods in the given folder.
+ /// The root folder containing mods.
+ public IEnumerable GetModFolders(string rootPath)
+ {
+ return new ModScanner(this.JsonHelper).GetModFolders(rootPath);
+ }
+
/// Get an update URL for an update key (if valid).
/// The update key.
public string GetUpdateUrl(string updateKey)