diff --git a/src/SMAPI.ModBuildConfig/DeployModTask.cs b/src/SMAPI.ModBuildConfig/DeployModTask.cs index f07bd8fe..3d6af22b 100644 --- a/src/SMAPI.ModBuildConfig/DeployModTask.cs +++ b/src/SMAPI.ModBuildConfig/DeployModTask.cs @@ -92,10 +92,10 @@ public class DeployModTask : Task { try { - string manifestPath = Path.Combine(this.ProjectDir, "manifest.json"); + string manifestPath = Path.Combine(this.ProjectDir, BundleFile.ManifestFileName); if (!new JsonHelper().ReadJsonFileIfExists(manifestPath, out Manifest rawManifest)) { - this.Log.LogError("[mod build package] The mod's manifest.json file doesn't exist."); + this.Log.LogError($"[mod build package] The mod's {BundleFile.ManifestFileName} file doesn't exist."); return false; } manifest = rawManifest; @@ -104,14 +104,14 @@ public class DeployModTask : Task { // log the inner exception, otherwise the message will be generic Exception exToShow = ex.InnerException ?? ex; - this.Log.LogError($"[mod build package] The mod's manifest.json file isn't valid JSON: {exToShow.Message}"); + this.Log.LogError($"[mod build package] The mod's {BundleFile.ManifestFileName} file isn't valid JSON: {exToShow.Message}"); return false; } // validate manifest fields if (!ManifestValidator.TryValidateFields(manifest, out string error)) { - this.Log.LogError($"[mod build package] The mod's manifest.json file is invalid: {error}"); + this.Log.LogError($"[mod build package] The mod's {BundleFile.ManifestFileName} file is invalid: {error}"); return false; } } @@ -301,19 +301,12 @@ public class DeployModTask : Task { foreach (var mod in modPackages) { - string relativePath = modPackages.Count == 1 + string folderPath = modPackages.Count == 1 ? outputPath : Path.Combine(outputPath, this.EscapeInvalidFilenameCharacters(mod.Key)); - foreach (var file in mod.Value.GetFiles()) - { - string fromPath = file.Value.FullName; - string toPath = Path.Combine(relativePath, file.Key); - - Directory.CreateDirectory(Path.GetDirectoryName(toPath)!); - - File.Copy(fromPath, toPath, overwrite: true); - } + foreach (BundleFile from in mod.Value.GetFiles()) + from.CopyToFolder(folderPath); } } @@ -330,21 +323,19 @@ public class DeployModTask : Task foreach (var mod in modPackages) { string modFolder = this.EscapeInvalidFilenameCharacters(mod.Key); - foreach (var file in mod.Value.GetFiles()) + foreach (BundleFile from in mod.Value.GetFiles()) { - string relativePath = file.Key; + string relativePath = from.RelativePath; + if (relativePath.Contains('\\')) relativePath = string.Join("/", PathUtilities.GetSegments(relativePath)); // zip files use forward slashes regardless of OS - FileInfo fileInfo = file.Value; - string archivePath = modPackages.Count == 1 ? $"{modFolder}/{relativePath}" : $"{this.ModFolderName}/{modFolder}/{relativePath}"; - using Stream fileStream = new FileStream(fileInfo.FullName, FileMode.Open, FileAccess.Read); using Stream fileStreamInZip = archive.CreateEntry(archivePath).Open(); - fileStream.CopyTo(fileStreamInZip); + from.CopyToStream(fileStreamInZip); } } } diff --git a/src/SMAPI.ModBuildConfig/Framework/BundleFile.cs b/src/SMAPI.ModBuildConfig/Framework/BundleFile.cs new file mode 100644 index 00000000..a6c4eca0 --- /dev/null +++ b/src/SMAPI.ModBuildConfig/Framework/BundleFile.cs @@ -0,0 +1,64 @@ +using System; +using System.IO; + +namespace StardewModdingAPI.ModBuildConfig.Framework; + +/// A file that should be deployed or zipped as part of a mod. +internal class BundleFile +{ + /********* + ** Accessors + *********/ + /// The name of the manifest file. + public const string ManifestFileName = "manifest.json"; + + /// The file's relative path within the mod. + public string RelativePath { get; } + + /// The file to copy from. + public FileInfo File { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The file's relative path within the mod. + /// The file to copy from. + public BundleFile(string relativePath, FileInfo file) + { + this.RelativePath = relativePath; + this.File = file; + } + + /// Get whether this entry is for the mod's manifest.json file. + public bool IsModManifest() + { + return BundleFile.IsModManifest(this.RelativePath); + } + + /// Copy the file into a destination folder. + /// The folder path to use as the base for the . + public void CopyToFolder(string folderPath) + { + string toPath = Path.Combine(folderPath, this.RelativePath); + + Directory.CreateDirectory(Path.GetDirectoryName(toPath)!); + this.File.CopyTo(toPath, overwrite: true); + } + + /// Copy the file's contents into a stream. + /// The stream into which to write the file's contents. + public void CopyToStream(Stream stream) + { + using FileStream fromStream = this.File.OpenRead(); + fromStream.CopyTo(stream); + } + + /// Get whether a relative path is for the mod's manifest.json file. + /// The relative path within the mod folder within the mod's folder. + public static bool IsModManifest(string relativePath) + { + return string.Equals(relativePath, BundleFile.ManifestFileName, StringComparison.OrdinalIgnoreCase); + } +} diff --git a/src/SMAPI.ModBuildConfig/Framework/ContentPackFileManager.cs b/src/SMAPI.ModBuildConfig/Framework/ContentPackFileManager.cs index 4e222c77..6d6b6e23 100644 --- a/src/SMAPI.ModBuildConfig/Framework/ContentPackFileManager.cs +++ b/src/SMAPI.ModBuildConfig/Framework/ContentPackFileManager.cs @@ -18,11 +18,8 @@ internal class ContentPackFileManager : IModFileManager /********* ** Fields *********/ - /// The name of the manifest file. - private readonly string ManifestFileName = "manifest.json"; - /// The files that are part of the package. - private readonly Dictionary Files = new(StringComparer.OrdinalIgnoreCase); + private readonly List Files = []; /********* @@ -44,37 +41,43 @@ internal class ContentPackFileManager : IModFileManager throw GetError($"that folder doesn't exist at {projectDirInfo.FullName}"); // collect files - foreach (FileInfo entry in projectDirInfo.GetFiles("*", SearchOption.AllDirectories)) + BundleFile manifestEntry = null; + foreach (FileInfo file in projectDirInfo.GetFiles("*", SearchOption.AllDirectories)) { - string relativePath = PathUtilities.GetRelativePath(projectDirInfo.FullName, entry.FullName); - FileInfo file = entry; + string relativePath = PathUtilities.GetRelativePath(projectDirInfo.FullName, file.FullName); if (!this.ShouldIgnore(file, relativePath, ignoreFilePaths, ignoreFilePatterns)) - this.Files[relativePath] = file; + { + BundleFile entry = new(relativePath, file); + this.Files.Add(entry); + + if (manifestEntry is null && entry.IsModManifest()) + manifestEntry = entry; + } } // validate manifest if (validateManifest) { // get manifest file - if (!this.Files.TryGetValue(this.ManifestFileName, out FileInfo manifestFile)) - throw GetError($"it has no {this.ManifestFileName} file"); + if (manifestEntry is null) + throw GetError($"it has no {BundleFile.ManifestFileName} file"); // parse file Manifest manifest; try { - new JsonHelper().ReadJsonFileIfExists(manifestFile.FullName, out Manifest rawManifest); + new JsonHelper().ReadJsonFileIfExists(manifestEntry.File.FullName, out Manifest rawManifest); manifest = rawManifest; } catch (JsonReaderException ex) { - throw GetError($"its {this.ManifestFileName} file isn't valid JSON: {ex.InnerException?.Message ?? ex.Message}"); + throw GetError($"its {BundleFile.ManifestFileName} file isn't valid JSON: {ex.InnerException?.Message ?? ex.Message}"); } // validate manifest fields if (!ManifestValidator.TryValidateFields(manifest, out string error)) - throw GetError($"its {this.ManifestFileName} file is invalid: {error}"); + throw GetError($"its {BundleFile.ManifestFileName} file is invalid: {error}"); // validate version if (version == null) @@ -82,7 +85,7 @@ internal class ContentPackFileManager : IModFileManager if (!SemanticVersion.TryParse(version, out ISemanticVersion requiredVersion)) throw GetError($"the provided Version value '{version}' isn't a valid semantic version"); if (manifest.Version.CompareTo(requiredVersion) != 0) - throw GetError($"its {this.ManifestFileName} has version '{manifest.Version}' instead of the required '{requiredVersion}'"); + throw GetError($"its {BundleFile.ManifestFileName} has version '{manifest.Version}' instead of the required '{requiredVersion}'"); } UserErrorException GetError(string reasonPhrase) @@ -92,9 +95,9 @@ internal class ContentPackFileManager : IModFileManager } /// - public IDictionary GetFiles() + public IEnumerable GetFiles() { - return new Dictionary(this.Files, StringComparer.OrdinalIgnoreCase); + return this.Files; } /// Get whether a content file should be ignored. diff --git a/src/SMAPI.ModBuildConfig/Framework/IModFileManager.cs b/src/SMAPI.ModBuildConfig/Framework/IModFileManager.cs index 1ce6cdc5..1eb0e470 100644 --- a/src/SMAPI.ModBuildConfig/Framework/IModFileManager.cs +++ b/src/SMAPI.ModBuildConfig/Framework/IModFileManager.cs @@ -1,11 +1,10 @@ using System.Collections.Generic; -using System.IO; namespace StardewModdingAPI.ModBuildConfig.Framework; /// Manages the files that are part of a mod in the release package. -public interface IModFileManager +internal interface IModFileManager { /// Get the files in the mod package. - public IDictionary GetFiles(); + public IEnumerable GetFiles(); } diff --git a/src/SMAPI.ModBuildConfig/Framework/MainModFileManager.cs b/src/SMAPI.ModBuildConfig/Framework/MainModFileManager.cs index 5bd15085..f25832fa 100644 --- a/src/SMAPI.ModBuildConfig/Framework/MainModFileManager.cs +++ b/src/SMAPI.ModBuildConfig/Framework/MainModFileManager.cs @@ -13,14 +13,11 @@ internal class MainModFileManager : IModFileManager /********* ** Fields *********/ - /// The name of the manifest file. - private readonly string ManifestFileName = "manifest.json"; - /// The files that are part of the package. - private readonly Dictionary Files = new(StringComparer.OrdinalIgnoreCase); + private readonly List Files = []; /// The file extensions used by assembly files. - private readonly ISet AssemblyFileExtensions = new HashSet(StringComparer.OrdinalIgnoreCase) + private readonly HashSet AssemblyFileExtensions = new(StringComparer.OrdinalIgnoreCase) { ".dll", ".exe", @@ -29,8 +26,8 @@ internal class MainModFileManager : IModFileManager }; /// The DLLs which match the type. - private readonly ISet GameDllNames = new HashSet - { + private readonly HashSet GameDllNames = + [ // SMAPI "0Harmony", "Mono.Cecil", @@ -56,7 +53,7 @@ internal class MainModFileManager : IModFileManager "Steamworks.NET", "TextCopy", "xTile" - }; + ]; /********* @@ -80,33 +77,36 @@ internal class MainModFileManager : IModFileManager throw new UserErrorException("Could not create mod package because no build output was found."); // collect files - foreach (Tuple entry in this.GetPossibleFiles(projectDir, targetDir)) + BundleFile manifestEntry = null; + foreach (BundleFile entry in this.GetPossibleFiles(projectDir, targetDir)) { - string relativePath = entry.Item1; - FileInfo file = entry.Item2; + if (!this.ShouldIgnore(entry.File, entry.RelativePath, ignoreFilePaths, ignoreFilePatterns, bundleAssemblyTypes, modDllName)) + { + this.Files.Add(entry); - if (!this.ShouldIgnore(file, relativePath, ignoreFilePaths, ignoreFilePatterns, bundleAssemblyTypes, modDllName)) - this.Files[relativePath] = file; + if (manifestEntry is null && entry.IsModManifest()) + manifestEntry = entry; + } } // check for required files if (validateRequiredModFiles) { // manifest - if (!this.Files.ContainsKey(this.ManifestFileName)) - throw new UserErrorException($"Could not create mod package because no {this.ManifestFileName} was found in the project or build output."); + if (manifestEntry is null) + throw new UserErrorException($"Could not create mod package because no {BundleFile.ManifestFileName} was found in the project or build output."); // DLL // ReSharper disable once SimplifyLinqExpression - if (!this.Files.Any(p => !p.Key.EndsWith(".dll"))) + if (!this.Files.Any(p => !p.RelativePath.EndsWith(".dll"))) throw new UserErrorException("Could not create mod package because no .dll file was found in the project or build output."); } } /// - public IDictionary GetFiles() + public IEnumerable GetFiles() { - return new Dictionary(this.Files, StringComparer.OrdinalIgnoreCase); + return this.Files; } @@ -117,15 +117,15 @@ internal class MainModFileManager : IModFileManager /// The folder containing the project files. /// The folder containing the build output. /// Returns tuples containing the relative path within the mod folder, and the file to copy to it. - private IEnumerable> GetPossibleFiles(string projectDir, string targetDir) + private IEnumerable GetPossibleFiles(string projectDir, string targetDir) { // project manifest bool hasProjectManifest = false; { - FileInfo manifest = new(Path.Combine(projectDir, this.ManifestFileName)); + FileInfo manifest = new(Path.Combine(projectDir, BundleFile.ManifestFileName)); if (manifest.Exists) { - yield return Tuple.Create(this.ManifestFileName, manifest); + yield return new BundleFile(BundleFile.ManifestFileName, manifest); hasProjectManifest = true; } } @@ -138,7 +138,7 @@ internal class MainModFileManager : IModFileManager foreach (FileInfo file in translationsFolder.EnumerateFiles("*", SearchOption.AllDirectories)) { string relativePath = PathUtilities.GetRelativePath(projectDir, file.FullName); - yield return Tuple.Create(relativePath, file); + yield return new BundleFile(relativePath, file); } hasProjectTranslations = true; } @@ -151,7 +151,7 @@ internal class MainModFileManager : IModFileManager foreach (FileInfo file in assetsFolder.EnumerateFiles("*", SearchOption.AllDirectories)) { string relativePath = PathUtilities.GetRelativePath(projectDir, file.FullName); - yield return Tuple.Create(relativePath, file); + yield return new BundleFile(relativePath, file); } hasAssetsFolder = true; } @@ -165,7 +165,7 @@ internal class MainModFileManager : IModFileManager string[] segments = PathUtilities.GetSegments(relativePath); // prefer project manifest/i18n/assets files - if (hasProjectManifest && this.EqualsInvariant(relativePath, this.ManifestFileName)) + if (hasProjectManifest && BundleFile.IsModManifest(relativePath)) continue; if (hasProjectTranslations && this.EqualsInvariant(segments[0], "i18n")) continue; @@ -173,7 +173,7 @@ internal class MainModFileManager : IModFileManager continue; // add file - yield return Tuple.Create(relativePath, file); + yield return new BundleFile(relativePath, file); } }