diff options
Diffstat (limited to 'src/SMAPI.ModBuildConfig/DeployModTask.cs')
-rw-r--r-- | src/SMAPI.ModBuildConfig/DeployModTask.cs | 176 |
1 files changed, 95 insertions, 81 deletions
diff --git a/src/SMAPI.ModBuildConfig/DeployModTask.cs b/src/SMAPI.ModBuildConfig/DeployModTask.cs index 2018ab06..a693fe32 100644 --- a/src/SMAPI.ModBuildConfig/DeployModTask.cs +++ b/src/SMAPI.ModBuildConfig/DeployModTask.cs @@ -2,11 +2,9 @@ using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; -using System.Linq; -using System.Web.Script.Serialization; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; -using StardewModdingAPI.Common; +using StardewModdingAPI.ModBuildConfig.Framework; namespace StardewModdingAPI.ModBuildConfig { @@ -16,25 +14,53 @@ namespace StardewModdingAPI.ModBuildConfig /********* ** Properties *********/ - /// <summary>The name of the manifest file.</summary> - private readonly string ManifestFileName = "manifest.json"; + /// <summary>The MSBuild platforms recognised by the build configuration.</summary> + private readonly HashSet<string> ValidPlatforms = new HashSet<string>(new[] { "OSX", "Unix", "Windows_NT" }, StringComparer.InvariantCultureIgnoreCase); + + /// <summary>The name of the game's main executable file.</summary> + private string GameExeName => this.Platform == "Windows_NT" + ? "Stardew Valley.exe" + : "StardewValley.exe"; + + /// <summary>The name of SMAPI's main executable file.</summary> + private readonly string SmapiExeName = "StardewModdingAPI.exe"; /********* ** Accessors *********/ - /// <summary>The mod files to pack.</summary> - [Required] - public ITaskItem[] Files { get; set; } - - /// <summary>The name of the mod.</summary> + /// <summary>The name of the mod folder.</summary> [Required] - public string ModName { get; set; } + public string ModFolderName { get; set; } /// <summary>The absolute or relative path to the folder which should contain the generated zip file.</summary> [Required] public string ModZipPath { get; set; } + /// <summary>The folder containing the project files.</summary> + [Required] + public string ProjectDir { get; set; } + + /// <summary>The folder containing the build output.</summary> + [Required] + public string TargetDir { get; set; } + + /// <summary>The folder containing the game files.</summary> + [Required] + public string GameDir { get; set; } + + /// <summary>The MSBuild OS value.</summary> + [Required] + public string Platform { get; set; } + + /// <summary>Whether to enable copying the mod files into the game's Mods folder.</summary> + [Required] + public bool EnableModDeploy { get; set; } + + /// <summary>Whether to enable the release zip.</summary> + [Required] + public bool EnableModZip { get; set; } + /********* ** Public methods @@ -43,33 +69,80 @@ namespace StardewModdingAPI.ModBuildConfig /// <returns>true if the task successfully executed; otherwise, false.</returns> public override bool Execute() { + if (!this.EnableModDeploy && !this.EnableModZip) + return true; // nothing to do + try { - string modVersion = this.GetManifestVersion(); - this.CreateReleaseZip(this.Files, this.ModName, modVersion, this.ModZipPath); + // validate context + if (!this.ValidPlatforms.Contains(this.Platform)) + throw new UserErrorException($"The mod build package doesn't recognise OS type '{this.Platform}'."); + if (!Directory.Exists(this.GameDir)) + throw new UserErrorException("The mod build package can't find your game path. See https://github.com/Pathoschild/SMAPI/blob/develop/docs/mod-build-config.md for help specifying it."); + if (!File.Exists(Path.Combine(this.GameDir, this.GameExeName))) + throw new UserErrorException($"The mod build package found a game folder at {this.GameDir}, but it doesn't contain the {this.GameExeName} file. If this folder is invalid, delete it and the package will autodetect another game install path."); + if (!File.Exists(Path.Combine(this.GameDir, this.SmapiExeName))) + throw new UserErrorException($"The mod build package found a game folder at {this.GameDir}, but it doesn't contain SMAPI. You need to install SMAPI before building the mod."); + + // get mod info + ModFileManager package = new ModFileManager(this.ProjectDir, this.TargetDir); + + // deploy mod files + if (this.EnableModDeploy) + { + string outputPath = Path.Combine(this.GameDir, "Mods", this.EscapeInvalidFilenameCharacters(this.ModFolderName)); + this.Log.LogMessage(MessageImportance.High, $"The mod build package is copying the mod files to {outputPath}..."); + this.CreateModFolder(package.GetFiles(), outputPath); + } + + // create release zip + if (this.EnableModZip) + { + this.Log.LogMessage(MessageImportance.High, $"The mod build package is generating a release zip at {this.ModZipPath} for {this.ModFolderName}..."); + this.CreateReleaseZip(package.GetFiles(), this.ModFolderName, package.GetManifestVersion(), this.ModZipPath); + } return true; } - catch (Exception ex) + catch (UserErrorException ex) { this.Log.LogErrorFromException(ex); return false; } + catch (Exception ex) + { + this.Log.LogError($"The mod build package failed trying to deploy the mod.\n{ex}"); + return false; + } } /********* ** Private methods *********/ + /// <summary>Copy the mod files into the game's mod folder.</summary> + /// <param name="files">The files to include.</param> + /// <param name="modFolderPath">The folder path to create with the mod files.</param> + private void CreateModFolder(IDictionary<string, FileInfo> files, string modFolderPath) + { + Directory.CreateDirectory(modFolderPath); + foreach (var entry in files) + { + string fromPath = entry.Value.FullName; + string toPath = Path.Combine(modFolderPath, entry.Key); + File.Copy(fromPath, toPath, overwrite: true); + } + } + /// <summary>Create a release zip in the recommended format for uploading to mod sites.</summary> /// <param name="files">The files to include.</param> /// <param name="modName">The name of the mod.</param> /// <param name="modVersion">The mod version string.</param> /// <param name="outputFolderPath">The absolute or relative path to the folder which should contain the generated zip file.</param> - private void CreateReleaseZip(ITaskItem[] files, string modName, string modVersion, string outputFolderPath) + private void CreateReleaseZip(IDictionary<string, FileInfo> files, string modName, string modVersion, string outputFolderPath) { // get names - string zipName = this.EscapeInvalidFilenameCharacters($"{modName}-{modVersion}.zip"); + string zipName = this.EscapeInvalidFilenameCharacters($"{modName} {modVersion}.zip"); string folderName = this.EscapeInvalidFilenameCharacters(modName); string zipPath = Path.Combine(outputFolderPath, zipName); @@ -78,84 +151,25 @@ namespace StardewModdingAPI.ModBuildConfig using (Stream zipStream = new FileStream(zipPath, FileMode.Create, FileAccess.Write)) using (ZipArchive archive = new ZipArchive(zipStream, ZipArchiveMode.Create)) { - foreach (ITaskItem file in files) + foreach (var fileEntry in files) { + string relativePath = fileEntry.Key; + FileInfo file = fileEntry.Value; + // get file info - string filePath = file.ItemSpec; - string entryName = folderName + '/' + file.GetMetadata("RecursiveDir") + file.GetMetadata("Filename") + file.GetMetadata("Extension"); + string filePath = file.FullName; + string entryName = folderName + '/' + relativePath.Replace(Path.DirectorySeparatorChar, '/'); if (new FileInfo(filePath).Directory.Name.Equals("i18n", StringComparison.InvariantCultureIgnoreCase)) entryName = Path.Combine("i18n", entryName); // add to zip using (Stream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read)) using (Stream fileStreamInZip = archive.CreateEntry(entryName).Open()) - { fileStream.CopyTo(fileStreamInZip); - } } } } - /// <summary>Get a semantic version from the mod manifest (if available).</summary> - /// <exception cref="InvalidOperationException">The manifest file wasn't found or is invalid.</exception> - private string GetManifestVersion() - { - // find manifest file - ITaskItem file = this.Files.FirstOrDefault(p => this.ManifestFileName.Equals(Path.GetFileName(p.ItemSpec), StringComparison.InvariantCultureIgnoreCase)); - if (file == null) - throw new InvalidOperationException($"The mod must include a {this.ManifestFileName} file."); - - // read content - string json = File.ReadAllText(file.ItemSpec); - if (string.IsNullOrWhiteSpace(json)) - throw new InvalidOperationException($"The mod's {this.ManifestFileName} file must not be empty."); - - // parse JSON - IDictionary<string, object> data; - try - { - data = this.Parse(json); - } - catch (Exception ex) - { - throw new InvalidOperationException($"The mod's {this.ManifestFileName} couldn't be parsed. It doesn't seem to be valid JSON.", ex); - } - - // get version field - object versionObj = data.ContainsKey("Version") ? data["Version"] : null; - if (versionObj == null) - throw new InvalidOperationException($"The mod's {this.ManifestFileName} must have a version field."); - - // get version string - if (versionObj is IDictionary<string, object> versionFields) // SMAPI 1.x - { - int major = versionFields.ContainsKey("MajorVersion") ? (int)versionFields["MajorVersion"] : 0; - int minor = versionFields.ContainsKey("MinorVersion") ? (int)versionFields["MinorVersion"] : 0; - int patch = versionFields.ContainsKey("PatchVersion") ? (int)versionFields["PatchVersion"] : 0; - string tag = versionFields.ContainsKey("Build") ? (string)versionFields["Build"] : null; - return new SemanticVersionImpl(major, minor, patch, tag).ToString(); - } - return new SemanticVersionImpl(versionObj.ToString()).ToString(); // SMAPI 2.0+ - } - - /// <summary>Get a case-insensitive dictionary matching the given JSON.</summary> - /// <param name="json">The JSON to parse.</param> - private IDictionary<string, object> Parse(string json) - { - IDictionary<string, object> MakeCaseInsensitive(IDictionary<string, object> dict) - { - foreach (var field in dict.ToArray()) - { - if (field.Value is IDictionary<string, object> value) - dict[field.Key] = MakeCaseInsensitive(value); - } - return new Dictionary<string, object>(dict, StringComparer.InvariantCultureIgnoreCase); - } - - IDictionary<string, object> data = (IDictionary<string, object>)new JavaScriptSerializer().DeserializeObject(json); - return MakeCaseInsensitive(data); - } - /// <summary>Get a copy of a filename with all invalid filename characters substituted.</summary> /// <param name="name">The filename.</param> private string EscapeInvalidFilenameCharacters(string name) |