using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.IO; using System.IO.Compression; using System.Security.Cryptography; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using System.Diagnostics; using Microsoft.WindowsAPICodePack.Dialogs; using System.Text.RegularExpressions; namespace AM2R_ModPacker { public partial class ModPacker : Form { private static readonly string ORIGINAL_MD5 = "f2b84fe5ba64cb64e284be1066ca08ee"; private bool isOriginalLoaded, isModLoaded, isApkLoaded, isLinuxLoaded; private string localPath, originalPath, modPath, apkPath, linuxPath; private static readonly string[] DATAFILES_BLACKLIST = { "data.win", "AM2R.exe", "D3DX9_43.dll", "game.unx" }; private static string saveFilePath = null; private ModProfileXML profile; public ModPacker() { InitializeComponent(); profile = new ModProfileXML("", 1, "", "", "", false, "", false, false, ""); // (1, "", "", false, "default", false, false); isOriginalLoaded = false; isModLoaded = false; isApkLoaded = false; isLinuxLoaded = false; localPath = Directory.GetCurrentDirectory(); originalPath = ""; modPath = ""; linuxPath = ""; apkPath = ""; } #region WinForms events private void OriginalButton_Click(object sender, EventArgs e) { // Open window to select AM2R 1.1 (isOriginalLoaded, originalPath) = SelectFile("Please select AM2R_11.zip", "zip", "zip files (*.zip)|*.zip"); OriginalLabel.Visible = isOriginalLoaded; UpdateCreateButton(); } private void ModButton_Click(object sender, EventArgs e) { // Open window to select modded AM2R (isModLoaded, modPath) = SelectFile("Please select your custom AM2R .zip", "zip", "zip files (*.zip)|*.zip"); ModLabel.Visible = isModLoaded; UpdateCreateButton(); } private void CreateButton_Click(object sender, EventArgs e) { if (NameTextBox.Text == "" || AuthorTextBox.Text == "" || versionTextBox.Text == "") { MessageBox.Show("Text field missing! Mod packaging aborted.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } CreateLabel.Visible = true; CreateLabel.Text = "Packaging mod(s)... This could take a while!"; string output; using (SaveFileDialog saveFile = new SaveFileDialog { InitialDirectory = localPath, Title = "Save Windows mod profile", Filter = "zip files (*.zip)|*.zip", AddExtension = true }) { if (saveFile.ShowDialog() == DialogResult.OK) { output = saveFile.FileName; } else { CreateLabel.Text = "Mod packaging aborted!"; return; } } CreateModPack("Windows", modPath, output); if (linuxCheckBox.Checked) { using (SaveFileDialog saveFile = new SaveFileDialog { InitialDirectory = localPath, Title = "Save Linux mod profile", Filter = "zip files (*.zip)|*.zip", AddExtension = true }) { if (saveFile.ShowDialog() == DialogResult.OK) { output = saveFile.FileName; } else { CreateLabel.Text = "Mod packaging aborted!"; return; } } CreateModPack("Linux", linuxPath, output); } CreateLabel.Text = "Mod package(s) created!"; } private void CreateModPack(string operatingSystem, string input, string output) { LoadProfileParameters(operatingSystem); // Cleanup in case of previous errors if (Directory.Exists(Path.GetTempPath() + "\\AM2RModPacker")) { Directory.Delete(Path.GetTempPath() + "\\AM2RModPacker", true); } // Create temp work folders string tempPath = "", tempOriginalPath = "", tempModPath = "", tempProfilePath = ""; // We might not have permission to access to the temp directory, so we need to catch the exception. try { tempPath = Directory.CreateDirectory(Path.GetTempPath() + "\\AM2RModPacker").FullName; tempOriginalPath = Directory.CreateDirectory(tempPath + "\\original").FullName; tempModPath = Directory.CreateDirectory(tempPath + "\\mod").FullName; tempProfilePath = Directory.CreateDirectory(tempPath + "\\profile").FullName; } catch (System.Security.SecurityException) { MessageBox.Show("Could not create temp directory! Please run the application with administrator rights.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); AbortPatch(); return; } // Extract 1.1 and modded AM2R to their own directories in temp work ZipFile.ExtractToDirectory(originalPath, tempOriginalPath); ZipFile.ExtractToDirectory(input, tempModPath); // Verify 1.1 with an MD5. If it does not match, exit cleanly and provide a warning window. try { string newMD5 = CalculateMD5(tempOriginalPath + "\\data.win"); if (newMD5 != ORIGINAL_MD5) { // Show error box MessageBox.Show("1.1 data.win does not meet MD5 checksum! Mod packaging aborted.\n1.1 MD5: " + ORIGINAL_MD5 + "\nYour MD5: " + newMD5, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); AbortPatch(); return; } } catch (FileNotFoundException) { // Show error message MessageBox.Show("data.win not found! Are you sure you selected AM2R 1.1? Mod packaging aborted.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); AbortPatch(); return; } // Create AM2R.exe and data.win patches if (profile.OperatingSystem == "Windows") { if (profile.UsesYYC) { CreatePatch(tempOriginalPath + "\\data.win", tempModPath + "\\AM2R.exe", tempProfilePath + "\\AM2R.xdelta"); } else { CreatePatch(tempOriginalPath + "\\data.win", tempModPath + "\\data.win", tempProfilePath + "\\data.xdelta"); CreatePatch(tempOriginalPath + "\\AM2R.exe", tempModPath + "\\AM2R.exe", tempProfilePath + "\\AM2R.xdelta"); } } else if (profile.OperatingSystem == "Linux") { CreatePatch(tempOriginalPath + "\\data.win", tempModPath + "\\assets\\game.unx", tempProfilePath + "\\game.xdelta"); CreatePatch(tempOriginalPath + "\\AM2R.exe", tempModPath + "\\AM2R", tempProfilePath + "\\AM2R.xdelta"); } // Create game.droid patch and wrapper if Android is supported if (profile.Android) { string tempAndroid = Directory.CreateDirectory(tempPath + "\\android").FullName; // Extract APK // - java -jar apktool.jar d "%~dp0AM2RWrapper_old.apk" // Process startInfo ProcessStartInfo procStartInfo = new ProcessStartInfo { FileName = "cmd.exe", WorkingDirectory = tempAndroid, Arguments = "/C java -jar \"" + localPath + "\\utilities\\android\\apktool.jar\" d -f -o \"" + tempAndroid + "\" \"" + apkPath + "\"", UseShellExecute = false, CreateNoWindow = true }; // Run process using (Process proc = new Process { StartInfo = procStartInfo }) { proc.Start(); proc.WaitForExit(); } // Create game.droid patch CreatePatch(tempOriginalPath + "\\data.win", tempAndroid + "\\assets\\game.droid", tempProfilePath + "\\droid.xdelta"); // Delete excess files in APK // Create whitelist string[] whitelist = { "splash.png", "portrait_splash.png" }; // Get directory DirectoryInfo androidAssets = new DirectoryInfo(tempAndroid + "\\assets"); // Delete files foreach (FileInfo file in androidAssets.GetFiles()) { if (file.Name.EndsWith(".ini") && file.Name != "modifiers.ini") { if (File.Exists(tempProfilePath + "\\AM2R.ini")) { // This shouldn't be a problem... normally... File.Delete(tempProfilePath + "\\AM2R.ini"); } File.Copy(file.FullName, tempProfilePath + "\\AM2R.ini"); } if (!whitelist.Contains(file.Name)) { File.Delete(file.FullName); } } foreach (DirectoryInfo dir in androidAssets.GetDirectories()) { Directory.Delete(dir.FullName, true); } // Create wrapper // Process startInfo // - java -jar apktool.jar b "%~dp0AM2RWrapper_old" -o "%~dp0AM2RWrapper.apk" ProcessStartInfo procStartInfo2 = new ProcessStartInfo { FileName = "cmd.exe", WorkingDirectory = tempAndroid, Arguments = "/C java -jar \"" + localPath + "\\utilities\\android\\apktool.jar\" b -f \"" + tempAndroid + "\" -o \"" + tempProfilePath + "\\AM2RWrapper.apk\"", UseShellExecute = false, CreateNoWindow = true }; // Run process using (Process proc = new Process { StartInfo = procStartInfo2 }) { proc.Start(); proc.WaitForExit(); } string tempAndroidProfilePath = tempProfilePath + "\\android"; Directory.CreateDirectory(tempAndroidProfilePath); File.Move(tempProfilePath + "\\AM2RWrapper.apk", tempAndroidProfilePath + "\\AM2RWrapper.apk"); if (File.Exists(tempProfilePath + "\\AM2R.ini")) File.Move(tempProfilePath + "\\AM2R.ini", tempAndroidProfilePath + "\\AM2R.ini"); } // Copy datafiles (exclude .ogg if custom music is not selected) DirectoryInfo dinfo = new DirectoryInfo(tempModPath); if (profile.OperatingSystem == "Linux") dinfo = new DirectoryInfo(tempModPath + "\\assets"); Directory.CreateDirectory(tempProfilePath + "\\files_to_copy"); if (profile.UsesCustomMusic) { // Copy files, excluding the blacklist CopyFilesRecursive(dinfo, DATAFILES_BLACKLIST, tempProfilePath + "\\files_to_copy"); } else { // Get list of 1.1's music files string[] musFiles = Directory.GetFiles(tempOriginalPath, "*.ogg").Select(file => Path.GetFileName(file)).ToArray(); if (profile.OperatingSystem == "Linux") musFiles = Directory.GetFiles(tempOriginalPath, "*.ogg").Select(file => Path.GetFileName(file).ToLower()).ToArray(); // Combine musFiles with the known datafiles for a blacklist string[] blacklist = musFiles.Concat(DATAFILES_BLACKLIST).ToArray(); // Copy files, excluding the blacklist CopyFilesRecursive(dinfo, blacklist, tempProfilePath + "\\files_to_copy"); } // Export profile as XML string xmlOutput = Serializer.Serialize(profile); File.WriteAllText(tempProfilePath + "\\profile.xml", xmlOutput); // Compress temp folder to .zip if (File.Exists(output)) { File.Delete(output); } ZipFile.CreateFromDirectory(tempProfilePath, output); // Delete temp folder Directory.Delete(tempPath, true); } private void ApkButton_Click(object sender, EventArgs e) { // Open window to select modded AM2R APK (isApkLoaded, apkPath) = SelectFile("Please select your custom AM2R .apk", "apk", "android application packages (*.apk)|*.apk"); ApkLabel.Visible = isApkLoaded; UpdateCreateButton(); } private void AndroidCheckBox_CheckedChanged(object sender, EventArgs e) { ApkButton.Enabled = AndroidCheckBox.Checked; UpdateCreateButton(); } private void SaveCheckBox_CheckedChanged(object sender, EventArgs e) { winSaveButton.Enabled = SaveCheckBox.Checked; saveTextBox.Enabled = SaveCheckBox.Checked; UpdateCreateButton(); } #endregion private void LoadProfileParameters(string operatingSystem) { profile.Name = NameTextBox.Text; profile.Author = AuthorTextBox.Text; profile.Version = versionTextBox.Text; profile.UsesCustomMusic = MusicCheckBox.Checked; profile.UsesYYC = YYCCheckBox.Checked; profile.Android = AndroidCheckBox.Checked; profile.ProfileNotes = modNotesTextBox.Text; profile.OperatingSystem = operatingSystem; if (SaveCheckBox.Checked && saveTextBox.Text != "") { profile.SaveLocation = saveTextBox.Text; } else { profile.SaveLocation = "%localappdata%/AM2R"; } if (operatingSystem == "Linux") { profile.SaveLocation = profile.SaveLocation.Replace("%localappdata%", "~/.config"); } } private void AbortPatch() { // Unload files isOriginalLoaded = false; isModLoaded = false; isApkLoaded = false; isLinuxLoaded = false; originalPath = ""; modPath = ""; apkPath = ""; linuxPath = ""; saveFilePath = null; // Set labels CreateLabel.Text = "Mod packaging aborted!"; OriginalLabel.Visible = false; ModLabel.Visible = false; ApkLabel.Visible = false; linuxLabel.Visible = false; // Remove temp directory if (Directory.Exists(Path.GetTempPath() + "\\AM2RModPacker")) { Directory.Delete(Path.GetTempPath() + "\\AM2RModPacker", true); } } private void linuxCheckBox_CheckedChanged(object sender, EventArgs e) { linuxButton.Enabled = linuxCheckBox.Checked; UpdateCreateButton(); } private void linuxButton_Click(object sender, EventArgs e) { // Open window to select modded Linux .zip (isLinuxLoaded, linuxPath) = SelectFile("Please select your custom Linux AM2R .zip", "zip", "zip archives (*.zip)|*.zip"); linuxLabel.Visible = isLinuxLoaded; UpdateCreateButton(); } private void CustomSaveDataButton_Click(object sender, EventArgs e) { bool wasSuccessfull = false; Regex saveRegex = new Regex(@"C:\\Users\\.*\\AppData\\Local\\"); //this is to ensure, that the save directory is valid. so far, this is only important for windows // we either use this nuget package. Or we choose a very bad native solution. Went for nuget instead. CommonOpenFileDialog dialog = new CommonOpenFileDialog(); dialog.InitialDirectory = Environment.GetEnvironmentVariable("LocalAppData"); dialog.IsFolderPicker = true; while (!wasSuccessfull) { if (dialog.ShowDialog() == CommonFileDialogResult.Ok) { Match match = saveRegex.Match(dialog.FileName); if (match.Success == false) MessageBox.Show("Invalid Save Directory! Please choose one in %LocalAppData%"); else { wasSuccessfull = true; saveFilePath = dialog.FileName.Replace(match.Value, "%LOCALAPPDATA%"); } } else { wasSuccessfull = true; saveFilePath = null; } } saveTextBox.Text = saveFilePath; } private void CopyFilesRecursive(DirectoryInfo source, string[] blacklist, string destination) { foreach (FileInfo file in source.GetFiles()) { if (!blacklist.Contains(file.Name)) { file.CopyTo(destination + "\\" + file.Name); } } foreach (DirectoryInfo dir in source.GetDirectories()) { string newDir = Directory.CreateDirectory(destination + "\\" + dir.Name).FullName; CopyFilesRecursive(dir, blacklist, newDir); } } private void UpdateCreateButton() { if (isOriginalLoaded && isModLoaded && (!AndroidCheckBox.Checked || isApkLoaded) && (!linuxCheckBox.Checked || isLinuxLoaded) && (!SaveCheckBox.Checked || saveTextBox.Text != "")) { CreateButton.Enabled = true; } else { CreateButton.Enabled = false; } } // Thanks, stackoverflow: https://stackoverflow.com/questions/10520048/calculate-md5-checksum-for-a-file private string CalculateMD5(string filename) { using (var stream = File.OpenRead(filename)) { using (var md5 = MD5.Create()) { var hash = md5.ComputeHash(stream); return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); } } } private void CreatePatch(string original, string modified, string output) { // Specify process start info ProcessStartInfo parameters = new ProcessStartInfo { FileName = localPath + "\\utilities\\xdelta\\xdelta3.exe", WorkingDirectory = localPath, UseShellExecute = false, CreateNoWindow = true, Arguments = "-f -e -s \"" + original + "\" \"" + modified + "\" \"" + output + "\"" }; // Launch process and wait for exit. using statement automatically disposes the object for us! using (Process proc = new Process { StartInfo = parameters }) { proc.Start(); proc.WaitForExit(); } } private (bool, string) SelectFile(string title, string extension, string filter) { using (OpenFileDialog fileFinder = new OpenFileDialog()) { fileFinder.InitialDirectory = localPath; fileFinder.Title = title; fileFinder.DefaultExt = extension; fileFinder.Filter = filter; fileFinder.CheckFileExists = true; fileFinder.CheckPathExists = true; fileFinder.Multiselect = false; if (fileFinder.ShowDialog() == DialogResult.OK) { string location = fileFinder.FileName; return (true, location); } else return (false, ""); } } } }