From d74b710833edd19d2df2c0847a033078fa71a06e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 1 May 2019 23:31:42 -0400 Subject: add mod type to mod scanner result --- src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs | 7 ++++++- .../Framework/ModScanning/ModScanner.cs | 20 +++++++++++++++----- src/SMAPI.Toolkit/Framework/ModScanning/ModType.cs | 18 ++++++++++++++++++ 3 files changed, 39 insertions(+), 6 deletions(-) create mode 100644 src/SMAPI.Toolkit/Framework/ModScanning/ModType.cs (limited to 'src/SMAPI.Toolkit/Framework/ModScanning') diff --git a/src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs b/src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs index bb467b36..adfee527 100644 --- a/src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs +++ b/src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs @@ -18,6 +18,9 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning /// The folder containing the mod's manifest.json. public DirectoryInfo Directory { get; } + /// The mod type. + public ModType Type { get; } + /// The mod manifest. public Manifest Manifest { get; } @@ -34,13 +37,15 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning /// Construct an instance. /// The root folder containing mods. /// The folder containing the mod's manifest.json. + /// The mod type. /// The mod manifest. /// The error which occurred parsing the manifest, if any. /// Whether the mod should be loaded by default. This should be false if it was found within a folder whose name starts with a dot. - public ModFolder(DirectoryInfo root, DirectoryInfo directory, Manifest manifest, string manifestParseError = null, bool shouldBeLoaded = true) + public ModFolder(DirectoryInfo root, DirectoryInfo directory, ModType type, Manifest manifest, string manifestParseError = null, bool shouldBeLoaded = true) { // save info this.Directory = directory; + this.Type = type; this.Manifest = manifest; this.ManifestParseError = manifestParseError; this.ShouldBeLoaded = shouldBeLoaded; diff --git a/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs index 0ab73d56..507e7be2 100644 --- a/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs +++ b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs @@ -65,10 +65,10 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning { FileInfo[] files = searchFolder.GetFiles("*", SearchOption.AllDirectories).Where(this.IsRelevant).ToArray(); if (!files.Any()) - return new ModFolder(root, searchFolder, null, "it's an empty folder."); + return new ModFolder(root, searchFolder, ModType.Invalid, 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."); + return new ModFolder(root, searchFolder, ModType.Xnb, null, "it's not a SMAPI mod (see https://smapi.io/xnb for info)."); + return new ModFolder(root, searchFolder, ModType.Invalid, null, "it contains files, but none of them are manifest.json."); } // read mod info @@ -98,7 +98,17 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning manifest.Author = this.StripNewlines(manifest.Author); } - return new ModFolder(root, manifestFile.Directory, manifest, manifestError); + // get mod type + ModType type = ModType.Invalid; + if (manifest != null) + { + type = !string.IsNullOrWhiteSpace(manifest.ContentPackFor?.UniqueID) + ? ModType.ContentPack + : ModType.Smapi; + } + + // build result + return new ModFolder(root, manifestFile.Directory, type, manifest, manifestError); } @@ -112,7 +122,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning { // 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); + yield return new ModFolder(root, folder, ModType.Invalid, null, "ignored folder because its name starts with a dot.", shouldBeLoaded: false); // recurse into subfolders else if (this.IsModSearchFolder(root, folder)) diff --git a/src/SMAPI.Toolkit/Framework/ModScanning/ModType.cs b/src/SMAPI.Toolkit/Framework/ModScanning/ModType.cs new file mode 100644 index 00000000..2ceb9e40 --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/ModScanning/ModType.cs @@ -0,0 +1,18 @@ +namespace StardewModdingAPI.Toolkit.Framework.ModScanning +{ + /// A general mod type. + public enum ModType + { + /// The mod is invalid and its type could not be determined. + Invalid, + + /// A mod which uses SMAPI directly. + Smapi, + + /// A mod which contains files loaded by a SMAPI mod. + ContentPack, + + /// A legacy mod which replaces game files directly. + Xnb + } +} -- cgit From 30cc7ac9161e4cea91c3b566a7edb5e438af98cb Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 1 May 2019 23:45:50 -0400 Subject: consolidate XNB mods when scanning mods --- .../Framework/ModScanning/ModScanner.cs | 27 ++++++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) (limited to 'src/SMAPI.Toolkit/Framework/ModScanning') diff --git a/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs index 507e7be2..ae0f292f 100644 --- a/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs +++ b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs @@ -52,6 +52,15 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning return this.GetModFolders(root, root); } + /// Extract information about all mods in the given folder. + /// The root folder containing mods. Only the will be searched, but this field allows it to be treated as a potential mod folder of its own. + /// The mod path to search. + // /// If the folder contains multiple XNB mods, treat them as subfolders of a single mod. This is useful when reading a single mod archive, as opposed to a mods folder. + public IEnumerable GetModFolders(string rootPath, string modPath) + { + return this.GetModFolders(root: new DirectoryInfo(rootPath), folder: new DirectoryInfo(modPath)); + } + /// Extract information from a mod folder. /// The root folder containing mods. /// The folder to search for a mod. @@ -120,17 +129,25 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning /// The folder to search for mods. public IEnumerable GetModFolders(DirectoryInfo root, DirectoryInfo folder) { + bool isRoot = folder.FullName == root.FullName; + // skip - if (folder.FullName != root.FullName && folder.Name.StartsWith(".")) + if (!isRoot && folder.Name.StartsWith(".")) yield return new ModFolder(root, folder, ModType.Invalid, null, "ignored folder because its name starts with a dot.", shouldBeLoaded: false); - // recurse into subfolders + // find mods in subfolders else if (this.IsModSearchFolder(root, folder)) { - foreach (DirectoryInfo subfolder in folder.EnumerateDirectories()) + ModFolder[] subfolders = folder.EnumerateDirectories().SelectMany(sub => this.GetModFolders(root, sub)).ToArray(); + if (!isRoot && subfolders.Length > 1 && subfolders.All(p => p.Type == ModType.Xnb)) + { + // if this isn't the root, and all subfolders are XNB mods, treat the whole folder as one XNB mod + yield return new ModFolder(folder, folder, ModType.Xnb, null, subfolders[0].ManifestParseError); + } + else { - foreach (ModFolder match in this.GetModFolders(root, subfolder)) - yield return match; + foreach (ModFolder subfolder in subfolders) + yield return subfolder; } } -- cgit From 83ae036e09aab6830fcf83ae3065628d4ab91143 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 4 May 2019 13:53:34 -0400 Subject: improve XNB mod and ignore file matching --- .../Framework/ModScanning/ModFolder.cs | 20 ++- .../Framework/ModScanning/ModParseError.cs | 24 ++++ .../Framework/ModScanning/ModScanner.cs | 141 ++++++++++++++++----- src/SMAPI.Toolkit/Framework/ModScanning/ModType.cs | 3 + 4 files changed, 147 insertions(+), 41 deletions(-) create mode 100644 src/SMAPI.Toolkit/Framework/ModScanning/ModParseError.cs (limited to 'src/SMAPI.Toolkit/Framework/ModScanning') diff --git a/src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs b/src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs index adfee527..4ce17c66 100644 --- a/src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs +++ b/src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs @@ -25,30 +25,38 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning public Manifest Manifest { get; } /// The error which occurred parsing the manifest, if any. - public string ManifestParseError { get; } + public ModParseError ManifestParseError { get; set; } - /// Whether the mod should be loaded by default. This is false if it was found within a folder whose name starts with a dot. - public bool ShouldBeLoaded { get; } + /// A human-readable message for the , if any. + public string ManifestParseErrorText { get; set; } /********* ** Public methods *********/ + /// Construct an instance. + /// The root folder containing mods. + /// The folder containing the mod's manifest.json. + /// The mod type. + /// The mod manifest. + public ModFolder(DirectoryInfo root, DirectoryInfo directory, ModType type, Manifest manifest) + : this(root, directory, type, manifest, ModParseError.None, null) { } + /// Construct an instance. /// The root folder containing mods. /// The folder containing the mod's manifest.json. /// The mod type. /// The mod manifest. /// The error which occurred parsing the manifest, if any. - /// Whether the mod should be loaded by default. This should be false if it was found within a folder whose name starts with a dot. - public ModFolder(DirectoryInfo root, DirectoryInfo directory, ModType type, Manifest manifest, string manifestParseError = null, bool shouldBeLoaded = true) + /// A human-readable message for the , if any. + public ModFolder(DirectoryInfo root, DirectoryInfo directory, ModType type, Manifest manifest, ModParseError manifestParseError, string manifestParseErrorText) { // save info this.Directory = directory; this.Type = type; this.Manifest = manifest; this.ManifestParseError = manifestParseError; - this.ShouldBeLoaded = shouldBeLoaded; + this.ManifestParseErrorText = manifestParseErrorText; // set display name this.DisplayName = manifest?.Name; diff --git a/src/SMAPI.Toolkit/Framework/ModScanning/ModParseError.cs b/src/SMAPI.Toolkit/Framework/ModScanning/ModParseError.cs new file mode 100644 index 00000000..b10510ff --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/ModScanning/ModParseError.cs @@ -0,0 +1,24 @@ +namespace StardewModdingAPI.Toolkit.Framework.ModScanning +{ + /// Indicates why a mod could not be parsed. + public enum ModParseError + { + /// No parse error. + None, + + /// The folder is empty or contains only ignored files. + EmptyFolder, + + /// The folder is ignored by convention. + IgnoredFolder, + + /// The mod's manifest.json could not be parsed. + ManifestInvalid, + + /// The folder contains non-ignored and non-XNB files, but none of them are manifest.json. + ManifestMissing, + + /// The folder is an XNB mod, which can't be loaded through SMAPI. + XnbMod + } +} diff --git a/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs index ae0f292f..54cb2b8b 100644 --- a/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs +++ b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.RegularExpressions; using StardewModdingAPI.Toolkit.Serialisation; using StardewModdingAPI.Toolkit.Serialisation.Models; @@ -17,20 +18,32 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning private readonly JsonHelper JsonHelper; /// A list of filesystem entry names to ignore when checking whether a folder should be treated as a mod. - private readonly HashSet IgnoreFilesystemEntries = new HashSet(StringComparer.InvariantCultureIgnoreCase) + private readonly HashSet IgnoreFilesystemEntries = new HashSet { - ".DS_Store", - "mcs", - "Thumbs.db" + // OS metadata files + new Regex(@"^__folder_managed_by_vortex$", RegexOptions.Compiled | RegexOptions.IgnoreCase), // Vortex mod manager + new Regex(@"^(?:__MACOSX|\._\.DS_Store|\.DS_Store|mcs)$", RegexOptions.Compiled | RegexOptions.IgnoreCase), // MacOS + new Regex(@"^(?:desktop\.ini|Thumbs\.db)$", RegexOptions.Compiled | RegexOptions.IgnoreCase), // Windows + new Regex(@"\.(?:url|lnk)$", RegexOptions.Compiled | RegexOptions.IgnoreCase), // Windows shortcut files + + // other + new Regex(@"\.(?:bmp|gif|jpeg|jpg|png|psd|tif)$", RegexOptions.Compiled | RegexOptions.IgnoreCase), // image files + new Regex(@"\.(?:md|rtf|txt)$", RegexOptions.Compiled | RegexOptions.IgnoreCase), // text files + new Regex(@"\.(?:backup|bak|old)$", RegexOptions.Compiled | RegexOptions.IgnoreCase) // backup file }; - /// 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. + /// The extensions for files which an XNB mod may contain. If a mod doesn't have a manifest.json and contains *only* these file extensions, it should be considered an XNB mod. private readonly HashSet PotentialXnbModExtensions = new HashSet(StringComparer.InvariantCultureIgnoreCase) { - ".md", - ".png", - ".txt", - ".xnb" + // XNB files + ".xgs", + ".xnb", + ".xsb", + ".xwb", + + // unpacking artifacts + ".json", + ".yaml" }; @@ -72,30 +85,36 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning // set appropriate invalid-mod error if (manifestFile == null) { - FileInfo[] files = searchFolder.GetFiles("*", SearchOption.AllDirectories).Where(this.IsRelevant).ToArray(); + FileInfo[] files = this.RecursivelyGetRelevantFiles(searchFolder).ToArray(); if (!files.Any()) - return new ModFolder(root, searchFolder, ModType.Invalid, null, "it's an empty folder."); - if (files.All(file => this.PotentialXnbModExtensions.Contains(file.Extension))) - return new ModFolder(root, searchFolder, ModType.Xnb, null, "it's not a SMAPI mod (see https://smapi.io/xnb for info)."); - return new ModFolder(root, searchFolder, ModType.Invalid, null, "it contains files, but none of them are manifest.json."); + return new ModFolder(root, searchFolder, ModType.Invalid, null, ModParseError.EmptyFolder, "it's an empty folder."); + if (files.All(this.IsPotentialXnbFile)) + return new ModFolder(root, searchFolder, ModType.Xnb, null, ModParseError.XnbMod, "it's not a SMAPI mod (see https://smapi.io/xnb for info)."); + return new ModFolder(root, searchFolder, ModType.Invalid, null, ModParseError.ManifestMissing, "it contains files, but none of them are manifest.json."); } // read mod info Manifest manifest = null; - string manifestError = null; + ModParseError error = ModParseError.None; + string errorText = null; { try { if (!this.JsonHelper.ReadJsonFileIfExists(manifestFile.FullName, out manifest) || manifest == null) - manifestError = "its manifest is invalid."; + { + error = ModParseError.ManifestInvalid; + errorText = "its manifest is invalid."; + } } catch (SParseException ex) { - manifestError = $"parsing its manifest failed: {ex.Message}"; + error = ModParseError.ManifestInvalid; + errorText = $"parsing its manifest failed: {ex.Message}"; } catch (Exception ex) { - manifestError = $"parsing its manifest failed:\n{ex}"; + error = ModParseError.ManifestInvalid; + errorText = $"parsing its manifest failed:\n{ex}"; } } @@ -117,7 +136,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning } // build result - return new ModFolder(root, manifestFile.Directory, type, manifest, manifestError); + return new ModFolder(root, manifestFile.Directory, type, manifest, error, errorText); } @@ -127,28 +146,30 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning /// Recursively extract information about all mods in the given folder. /// The root mod folder. /// The folder to search for mods. - public IEnumerable GetModFolders(DirectoryInfo root, DirectoryInfo folder) + private IEnumerable GetModFolders(DirectoryInfo root, DirectoryInfo folder) { bool isRoot = folder.FullName == root.FullName; // skip - if (!isRoot && folder.Name.StartsWith(".")) - yield return new ModFolder(root, folder, ModType.Invalid, null, "ignored folder because its name starts with a dot.", shouldBeLoaded: false); - - // find mods in subfolders - else if (this.IsModSearchFolder(root, folder)) + if (!isRoot) { - ModFolder[] subfolders = folder.EnumerateDirectories().SelectMany(sub => this.GetModFolders(root, sub)).ToArray(); - if (!isRoot && subfolders.Length > 1 && subfolders.All(p => p.Type == ModType.Xnb)) + if (folder.Name.StartsWith(".")) { - // if this isn't the root, and all subfolders are XNB mods, treat the whole folder as one XNB mod - yield return new ModFolder(folder, folder, ModType.Xnb, null, subfolders[0].ManifestParseError); - } - else - { - foreach (ModFolder subfolder in subfolders) - yield return subfolder; + yield return new ModFolder(root, folder, ModType.Ignored, null, ModParseError.IgnoredFolder, "ignored folder because its name starts with a dot."); + yield break; } + if (!this.IsRelevant(folder)) + yield break; + } + + // find mods in subfolders + if (this.IsModSearchFolder(root, folder)) + { + IEnumerable subfolders = folder.EnumerateDirectories().SelectMany(sub => this.GetModFolders(root, sub)); + if (!isRoot) + subfolders = this.TryConsolidate(root, folder, subfolders.ToArray()); + foreach (ModFolder subfolder in subfolders) + yield return subfolder; } // treat as mod folder @@ -156,6 +177,26 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning yield return this.ReadFolder(root, folder); } + /// Consolidate adjacent folders into one mod folder, if possible. + /// The folder containing both parent and subfolders. + /// The parent folder to consolidate, if possible. + /// The subfolders to consolidate, if possible. + private IEnumerable TryConsolidate(DirectoryInfo root, DirectoryInfo parentFolder, ModFolder[] subfolders) + { + if (subfolders.Length > 1) + { + // a collection of empty folders + if (subfolders.All(p => p.ManifestParseError == ModParseError.EmptyFolder)) + return new[] { new ModFolder(root, parentFolder, ModType.Invalid, null, ModParseError.EmptyFolder, subfolders[0].ManifestParseErrorText) }; + + // an XNB mod + if (subfolders.All(p => p.Type == ModType.Xnb || p.ManifestParseError == ModParseError.EmptyFolder)) + return new[] { new ModFolder(root, parentFolder, ModType.Xnb, null, ModParseError.XnbMod, subfolders[0].ManifestParseErrorText) }; + } + + return subfolders; + } + /// Find the manifest for a mod folder. /// The folder to search. private FileInfo FindManifest(DirectoryInfo folder) @@ -193,11 +234,41 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning return subfolders.Any() && !files.Any(); } + /// Recursively get all relevant files in a folder based on the result of . + /// The root folder to search. + private IEnumerable RecursivelyGetRelevantFiles(DirectoryInfo folder) + { + foreach (FileSystemInfo entry in folder.GetFileSystemInfos()) + { + if (!this.IsRelevant(entry)) + continue; + + if (entry is FileInfo file) + yield return file; + + if (entry is DirectoryInfo subfolder) + { + foreach (FileInfo subfolderFile in this.RecursivelyGetRelevantFiles(subfolder)) + yield return subfolderFile; + } + } + } + /// Get whether a file or folder is relevant when deciding how to process a mod folder. /// The file or folder. private bool IsRelevant(FileSystemInfo entry) { - return !this.IgnoreFilesystemEntries.Contains(entry.Name); + return !this.IgnoreFilesystemEntries.Any(p => p.IsMatch(entry.Name)); + } + + /// Get whether a file is potentially part of an XNB mod. + /// The file. + private bool IsPotentialXnbFile(FileInfo entry) + { + if (!this.IsRelevant(entry)) + return true; + + return this.PotentialXnbModExtensions.Contains(entry.Extension); // use EndsWith to handle cases like image..png } /// Strip newlines from a string. diff --git a/src/SMAPI.Toolkit/Framework/ModScanning/ModType.cs b/src/SMAPI.Toolkit/Framework/ModScanning/ModType.cs index 2ceb9e40..bc86edb6 100644 --- a/src/SMAPI.Toolkit/Framework/ModScanning/ModType.cs +++ b/src/SMAPI.Toolkit/Framework/ModScanning/ModType.cs @@ -6,6 +6,9 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning /// The mod is invalid and its type could not be determined. Invalid, + /// The folder is ignored by convention. + Ignored, + /// A mod which uses SMAPI directly. Smapi, -- cgit From fd77ae93d59222d70c86aebfc044f3af11063372 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 9 Aug 2019 01:18:05 -0400 Subject: fix typos and inconsistent spelling --- src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs | 2 +- src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) (limited to 'src/SMAPI.Toolkit/Framework/ModScanning') diff --git a/src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs b/src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs index 4ce17c66..d0df09a1 100644 --- a/src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs +++ b/src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using StardewModdingAPI.Toolkit.Serialisation.Models; +using StardewModdingAPI.Toolkit.Serialization.Models; using StardewModdingAPI.Toolkit.Utilities; namespace StardewModdingAPI.Toolkit.Framework.ModScanning diff --git a/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs index 54cb2b8b..f11cc1a7 100644 --- a/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs +++ b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs @@ -3,8 +3,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; -using StardewModdingAPI.Toolkit.Serialisation; -using StardewModdingAPI.Toolkit.Serialisation.Models; +using StardewModdingAPI.Toolkit.Serialization; +using StardewModdingAPI.Toolkit.Serialization.Models; namespace StardewModdingAPI.Toolkit.Framework.ModScanning { @@ -118,7 +118,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning } } - // normalise display fields + // normalize display fields if (manifest != null) { manifest.Name = this.StripNewlines(manifest.Name); -- cgit