summaryrefslogtreecommitdiff
path: root/src/SMAPI.Toolkit/Framework/ModScanning
diff options
context:
space:
mode:
authorJesse Plamondon-Willard <Pathoschild@users.noreply.github.com>2019-09-13 17:22:45 -0400
committerJesse Plamondon-Willard <Pathoschild@users.noreply.github.com>2019-09-13 17:22:45 -0400
commit125bcbee56bf40cf82abc7fdb502f8cbc18546cf (patch)
tree788997dd4683867b6e32e307c17c855bd7209d98 /src/SMAPI.Toolkit/Framework/ModScanning
parent56726073ba65a018312bcd9db7072381073de315 (diff)
downloadSMAPI-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.cs64
-rw-r--r--src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs183
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", "");
+ }
+ }
+}