diff options
-rw-r--r-- | src/SMAPI.Tests/Core/ModResolverTests.cs | 8 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModLoading/ModResolver.cs | 40 | ||||
-rw-r--r-- | src/SMAPI/Program.cs | 16 | ||||
-rw-r--r-- | src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModFolder.cs | 60 | ||||
-rw-r--r-- | src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs | 103 | ||||
-rw-r--r-- | src/StardewModdingAPI.Toolkit/ModToolkit.cs | 16 |
6 files changed, 200 insertions, 43 deletions
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 *********/ /// <summary>Get manifest metadata for each folder in the given root path.</summary> + /// <param name="toolkit">The mod toolkit.</param> /// <param name="rootPath">The root path to search for mods.</param> - /// <param name="jsonHelper">The JSON helper with which to read manifests.</param> /// <param name="modDatabase">Handles access to SMAPI's internal mod metadata list.</param> /// <returns>Returns the manifests by relative folder.</returns> - public IEnumerable<IModMetadata> ReadManifests(string rootPath, JsonHelper jsonHelper, ModDatabase modDatabase) + public IEnumerable<IModMetadata> 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<Manifest>(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) }; - /// <summary>Encapsulates SMAPI's JSON file parsing.</summary> - private readonly JsonHelper JsonHelper = new JsonHelper(); + /// <summary>The mod toolkit used for generic mod interactions.</summary> + 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 /// <param name="mods">The mods for which to reload translations.</param> private void ReloadTranslations(IEnumerable<IModMetadata> 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 +{ + /// <summary>The info about a mod read from its folder.</summary> + public class ModFolder + { + /********* + ** Accessors + *********/ + /// <summary>The Mods subfolder containing this mod.</summary> + public DirectoryInfo SearchDirectory { get; } + + /// <summary>The folder containing manifest.json.</summary> + public DirectoryInfo ActualDirectory { get; } + + /// <summary>The mod manifest.</summary> + public Manifest Manifest { get; } + + /// <summary>The error which occurred parsing the manifest, if any.</summary> + public string ManifestParseError { get; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance when a mod wasn't found in a folder.</summary> + /// <param name="searchDirectory">The directory that was searched.</param> + public ModFolder(DirectoryInfo searchDirectory) + { + this.SearchDirectory = searchDirectory; + } + + /// <summary>Construct an instance.</summary> + /// <param name="searchDirectory">The Mods subfolder containing this mod.</param> + /// <param name="actualDirectory">The folder containing manifest.json.</param> + /// <param name="manifest">The mod manifest.</param> + /// <param name="manifestParseError">The error which occurred parsing the manifest, if any.</param> + public ModFolder(DirectoryInfo searchDirectory, DirectoryInfo actualDirectory, Manifest manifest, string manifestParseError = null) + { + this.SearchDirectory = searchDirectory; + this.ActualDirectory = actualDirectory; + this.Manifest = manifest; + this.ManifestParseError = manifestParseError; + } + + /// <summary>Get the update keys for a mod.</summary> + /// <param name="manifest">The mod manifest.</param> + public IEnumerable<string> 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 +{ + /// <summary>Scans folders for mod data.</summary> + public class ModScanner + { + /********* + ** Properties + *********/ + /// <summary>The JSON helper with which to read manifests.</summary> + private readonly JsonHelper JsonHelper; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="jsonHelper">The JSON helper with which to read manifests.</param> + public ModScanner(JsonHelper jsonHelper) + { + this.JsonHelper = jsonHelper; + } + + /// <summary>Extract information about all mods in the given folder.</summary> + /// <param name="rootPath">The root folder containing mods.</param> + public IEnumerable<ModFolder> GetModFolders(string rootPath) + { + foreach (DirectoryInfo folder in new DirectoryInfo(rootPath).EnumerateDirectories()) + yield return this.ReadFolder(rootPath, folder); + } + + /// <summary>Extract information from a mod folder.</summary> + /// <param name="rootPath">The root folder containing mods.</param> + /// <param name="searchFolder">The folder to search for a mod.</param> + 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<Manifest>(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 + *********/ + /// <summary>Find the manifest for a mod folder.</summary> + /// <param name="folder">The folder to search.</param> + 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 { @@ -28,6 +30,13 @@ namespace StardewModdingAPI.Toolkit /********* + ** Accessors + *********/ + /// <summary>Encapsulates SMAPI's JSON parsing.</summary> + public JsonHelper JsonHelper { get; } = new JsonHelper(); + + + /********* ** Public methods *********/ /// <summary>Construct an instance.</summary> @@ -53,6 +62,13 @@ namespace StardewModdingAPI.Toolkit return new ModDatabase(records, this.GetUpdateUrl); } + /// <summary>Extract information about all mods in the given folder.</summary> + /// <param name="rootPath">The root folder containing mods.</param> + public IEnumerable<ModFolder> GetModFolders(string rootPath) + { + return new ModScanner(this.JsonHelper).GetModFolders(rootPath); + } + /// <summary>Get an update URL for an update key (if valid).</summary> /// <param name="updateKey">The update key.</param> public string GetUpdateUrl(string updateKey) |