using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using StardewModdingAPI.ModBuildConfig.Framework;
namespace StardewModdingAPI.ModBuildConfig
{
/// A build task which deploys the mod files and prepares a release zip.
public class DeployModTask : Task
{
/*********
** Properties
*********/
/// The MSBuild platforms recognised by the build configuration.
private readonly HashSet ValidPlatforms = new HashSet(new[] { "OSX", "Unix", "Windows_NT" }, StringComparer.InvariantCultureIgnoreCase);
/// The name of the game's main executable file.
private string GameExeName => this.Platform == "Windows_NT"
? "Stardew Valley.exe"
: "StardewValley.exe";
/// The name of SMAPI's main executable file.
private readonly string SmapiExeName = "StardewModdingAPI.exe";
/*********
** Accessors
*********/
/// The name of the mod folder.
[Required]
public string ModFolderName { get; set; }
/// The absolute or relative path to the folder which should contain the generated zip file.
[Required]
public string ModZipPath { get; set; }
/// The folder containing the project files.
[Required]
public string ProjectDir { get; set; }
/// The folder containing the build output.
[Required]
public string TargetDir { get; set; }
/// The folder containing the game files.
[Required]
public string GameDir { get; set; }
/// The MSBuild OS value.
[Required]
public string Platform { get; set; }
/// Whether to enable copying the mod files into the game's Mods folder.
[Required]
public bool EnableModDeploy { get; set; }
/// Whether to enable the release zip.
[Required]
public bool EnableModZip { get; set; }
/*********
** Public methods
*********/
/// When overridden in a derived class, executes the task.
/// true if the task successfully executed; otherwise, false.
public override bool Execute()
{
if (!this.EnableModDeploy && !this.EnableModZip)
return true; // nothing to do
try
{
// 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 (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
*********/
/// Copy the mod files into the game's mod folder.
/// The files to include.
/// The folder path to create with the mod files.
private void CreateModFolder(IDictionary 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);
}
}
/// Create a release zip in the recommended format for uploading to mod sites.
/// The files to include.
/// The name of the mod.
/// The mod version string.
/// The absolute or relative path to the folder which should contain the generated zip file.
private void CreateReleaseZip(IDictionary files, string modName, string modVersion, string outputFolderPath)
{
// get names
string zipName = this.EscapeInvalidFilenameCharacters($"{modName} {modVersion}.zip");
string folderName = this.EscapeInvalidFilenameCharacters(modName);
string zipPath = Path.Combine(outputFolderPath, zipName);
// create zip file
Directory.CreateDirectory(outputFolderPath);
using (Stream zipStream = new FileStream(zipPath, FileMode.Create, FileAccess.Write))
using (ZipArchive archive = new ZipArchive(zipStream, ZipArchiveMode.Create))
{
foreach (var fileEntry in files)
{
string relativePath = fileEntry.Key;
FileInfo file = fileEntry.Value;
// get file info
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);
}
}
}
/// Get a copy of a filename with all invalid filename characters substituted.
/// The filename.
private string EscapeInvalidFilenameCharacters(string name)
{
foreach (char invalidChar in Path.GetInvalidFileNameChars())
name = name.Replace(invalidChar, '.');
return name;
}
}
}