diff options
author | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2019-09-13 17:22:45 -0400 |
---|---|---|
committer | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2019-09-13 17:22:45 -0400 |
commit | 125bcbee56bf40cf82abc7fdb502f8cbc18546cf (patch) | |
tree | 788997dd4683867b6e32e307c17c855bd7209d98 /src/SMAPI.Toolkit/Framework/ModScanning | |
parent | 56726073ba65a018312bcd9db7072381073de315 (diff) | |
download | SMAPI-125bcbee56bf40cf82abc7fdb502f8cbc18546cf.tar.gz SMAPI-125bcbee56bf40cf82abc7fdb502f8cbc18546cf.tar.bz2 SMAPI-125bcbee56bf40cf82abc7fdb502f8cbc18546cf.zip |
migrate to new project file format
Diffstat (limited to 'src/SMAPI.Toolkit/Framework/ModScanning')
-rw-r--r-- | src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs | 64 | ||||
-rw-r--r-- | src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs | 183 |
2 files changed, 247 insertions, 0 deletions
diff --git a/src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs b/src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs new file mode 100644 index 00000000..bb467b36 --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using StardewModdingAPI.Toolkit.Serialisation.Models; +using StardewModdingAPI.Toolkit.Utilities; + +namespace StardewModdingAPI.Toolkit.Framework.ModScanning +{ + /// <summary>The info about a mod read from its folder.</summary> + public class ModFolder + { + /********* + ** Accessors + *********/ + /// <summary>A suggested display name for the mod folder.</summary> + public string DisplayName { get; } + + /// <summary>The folder containing the mod's manifest.json.</summary> + public DirectoryInfo Directory { 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; } + + /// <summary>Whether the mod should be loaded by default. This is <c>false</c> if it was found within a folder whose name starts with a dot.</summary> + public bool ShouldBeLoaded { get; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="root">The root folder containing mods.</param> + /// <param name="directory">The folder containing the mod's manifest.json.</param> + /// <param name="manifest">The mod manifest.</param> + /// <param name="manifestParseError">The error which occurred parsing the manifest, if any.</param> + /// <param name="shouldBeLoaded">Whether the mod should be loaded by default. This should be <c>false</c> if it was found within a folder whose name starts with a dot.</param> + public ModFolder(DirectoryInfo root, DirectoryInfo directory, Manifest manifest, string manifestParseError = null, bool shouldBeLoaded = true) + { + // save info + this.Directory = directory; + this.Manifest = manifest; + this.ManifestParseError = manifestParseError; + this.ShouldBeLoaded = shouldBeLoaded; + + // set display name + this.DisplayName = manifest?.Name; + if (string.IsNullOrWhiteSpace(this.DisplayName)) + this.DisplayName = PathUtilities.GetRelativePath(root.FullName, directory.FullName); + } + + /// <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/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs new file mode 100644 index 00000000..0ab73d56 --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs @@ -0,0 +1,183 @@ +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 + { + /********* + ** Fields + *********/ + /// <summary>The JSON helper with which to read manifests.</summary> + private readonly JsonHelper JsonHelper; + + /// <summary>A list of filesystem entry names to ignore when checking whether a folder should be treated as a mod.</summary> + private readonly HashSet<string> IgnoreFilesystemEntries = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase) + { + ".DS_Store", + "mcs", + "Thumbs.db" + }; + + /// <summary>The extensions for files which an XNB mod may contain. If a mod contains *only* these file extensions, it should be considered an XNB mod.</summary> + private readonly HashSet<string> PotentialXnbModExtensions = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase) + { + ".md", + ".png", + ".txt", + ".xnb" + }; + + + /********* + ** 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) + { + DirectoryInfo root = new DirectoryInfo(rootPath); + return this.GetModFolders(root, root); + } + + /// <summary>Extract information from a mod folder.</summary> + /// <param name="root">The root folder containing mods.</param> + /// <param name="searchFolder">The folder to search for a mod.</param> + public ModFolder ReadFolder(DirectoryInfo root, DirectoryInfo searchFolder) + { + // find manifest.json + FileInfo manifestFile = this.FindManifest(searchFolder); + + // set appropriate invalid-mod error + if (manifestFile == null) + { + FileInfo[] files = searchFolder.GetFiles("*", SearchOption.AllDirectories).Where(this.IsRelevant).ToArray(); + if (!files.Any()) + return new ModFolder(root, searchFolder, null, "it's an empty folder."); + if (files.All(file => this.PotentialXnbModExtensions.Contains(file.Extension))) + return new ModFolder(root, searchFolder, null, "it's not a SMAPI mod (see https://smapi.io/xnb for info)."); + return new ModFolder(root, searchFolder, null, "it contains files, but none of them are manifest.json."); + } + + // read mod info + Manifest manifest = null; + string manifestError = null; + { + try + { + if (!this.JsonHelper.ReadJsonFileIfExists<Manifest>(manifestFile.FullName, out manifest) || manifest == null) + manifestError = "its manifest is invalid."; + } + catch (SParseException ex) + { + manifestError = $"parsing its manifest failed: {ex.Message}"; + } + catch (Exception ex) + { + manifestError = $"parsing its manifest failed:\n{ex}"; + } + } + + // normalise display fields + if (manifest != null) + { + manifest.Name = this.StripNewlines(manifest.Name); + manifest.Description = this.StripNewlines(manifest.Description); + manifest.Author = this.StripNewlines(manifest.Author); + } + + return new ModFolder(root, manifestFile.Directory, manifest, manifestError); + } + + + /********* + ** Private methods + *********/ + /// <summary>Recursively extract information about all mods in the given folder.</summary> + /// <param name="root">The root mod folder.</param> + /// <param name="folder">The folder to search for mods.</param> + public IEnumerable<ModFolder> GetModFolders(DirectoryInfo root, DirectoryInfo folder) + { + // skip + if (folder.FullName != root.FullName && folder.Name.StartsWith(".")) + yield return new ModFolder(root, folder, null, "ignored folder because its name starts with a dot.", shouldBeLoaded: false); + + // recurse into subfolders + else if (this.IsModSearchFolder(root, folder)) + { + foreach (DirectoryInfo subfolder in folder.EnumerateDirectories()) + { + foreach (ModFolder match in this.GetModFolders(root, subfolder)) + yield return match; + } + } + + // treat as mod folder + else + yield return this.ReadFolder(root, folder); + } + + /// <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; + } + } + + /// <summary>Get whether a given folder should be treated as a search folder (i.e. look for subfolders containing mods).</summary> + /// <param name="root">The root mod folder.</param> + /// <param name="folder">The folder to search for mods.</param> + private bool IsModSearchFolder(DirectoryInfo root, DirectoryInfo folder) + { + if (root.FullName == folder.FullName) + return true; + + DirectoryInfo[] subfolders = folder.GetDirectories().Where(this.IsRelevant).ToArray(); + FileInfo[] files = folder.GetFiles().Where(this.IsRelevant).ToArray(); + return subfolders.Any() && !files.Any(); + } + + /// <summary>Get whether a file or folder is relevant when deciding how to process a mod folder.</summary> + /// <param name="entry">The file or folder.</param> + private bool IsRelevant(FileSystemInfo entry) + { + return !this.IgnoreFilesystemEntries.Contains(entry.Name); + } + + /// <summary>Strip newlines from a string.</summary> + /// <param name="input">The input to strip.</param> + private string StripNewlines(string input) + { + return input?.Replace("\r", "").Replace("\n", ""); + } + } +} |