/// <summary> /// Creates an APK of the selected <paramref name="profile"/>. /// </summary> /// <param name="profile"><see cref="ProfileXML"/> to be compiled into an APK.</param> /// <param name="useHqMusic">Wether to create the APK with high quality music or not.</param> /// <param name="progress">Provides the current progress of this method.</param> public static void CreateAPK(ProfileXML profile, bool useHqMusic, IProgress <int> progress) { // Overall safety check just in case of bad situations if (!profile.SupportsAndroid) { progress.Report(100); return; } log.Info("Creating Android APK for profile " + profile.Name + "."); // Create working dir after some cleanup string apktoolPath = CrossPlatformOperations.CURRENTPATH + "/PatchData/utilities/android/apktool.jar", uberPath = CrossPlatformOperations.CURRENTPATH + "/PatchData/utilities/android/uber-apk-signer.jar", tempDir = new DirectoryInfo(CrossPlatformOperations.CURRENTPATH + "/temp").FullName, dataPath = CrossPlatformOperations.CURRENTPATH + profile.DataPath; if (Directory.Exists(tempDir)) { Directory.Delete(tempDir, true); } Directory.CreateDirectory(tempDir); log.Info("Cleanup, variables, and working directory created."); progress.Report(14); // Decompile AM2RWrapper.apk CrossPlatformOperations.RunJavaJar("\"" + apktoolPath + "\" d \"" + dataPath + "/android/AM2RWrapper.apk\"", tempDir); log.Info("AM2RWrapper decompiled."); progress.Report(28); // Add datafiles: 1.1, new datafiles, hq music, am2r.ini string workingDir = tempDir + "/AM2RWrapper/assets"; ZipFile.ExtractToDirectory(CrossPlatformOperations.CURRENTPATH + "/AM2R_11.zip", workingDir); HelperMethods.DirectoryCopy(dataPath + "/files_to_copy", workingDir); if (useHqMusic) { HelperMethods.DirectoryCopy(CrossPlatformOperations.CURRENTPATH + "/PatchData/data/HDR_HQ_in-game_music", workingDir); } // Yes, I'm aware this is dumb. If you've got any better ideas for how to copy a seemingly randomly named .ini from this folder to the APK, please let me know. foreach (FileInfo file in new DirectoryInfo(dataPath).GetFiles().Where(f => f.Name.EndsWith("ini"))) { File.Copy(file.FullName, workingDir + "/" + file.Name); } log.Info("AM2R_11.zip extracted and datafiles copied into AM2RWrapper."); progress.Report(42); // Patch data.win to game.droid CrossPlatformOperations.ApplyXdeltaPatch(workingDir + "/data.win", dataPath + "/droid.xdelta", workingDir + "/game.droid"); log.Info("game.droid successfully patched."); progress.Report(56); // Delete unnecessary files File.Delete(workingDir + "/AM2R.exe"); File.Delete(workingDir + "/D3DX9_43.dll"); File.Delete(workingDir + "/explanations.txt"); File.Delete(workingDir + "/modifiers.ini"); File.Delete(workingDir + "/readme.txt"); File.Delete(workingDir + "/data.win"); Directory.Delete(workingDir + "/mods", true); Directory.Delete(workingDir + "/lang/headers", true); if (OS.IsLinux) { File.Delete(workingDir + "/icon.png"); } // Modify apktool.yml to NOT compress ogg files string apktoolText = File.ReadAllText(workingDir + "/../apktool.yml"); apktoolText = apktoolText.Replace("doNotCompress:", "doNotCompress:\n- ogg"); File.WriteAllText(workingDir + "/../apktool.yml", apktoolText); log.Info("Unnecessary files removed, apktool.yml modified to prevent ogg compression."); progress.Report(70); // Rebuild APK CrossPlatformOperations.RunJavaJar("\"" + apktoolPath + "\" b AM2RWrapper -o \"" + profile.Name + ".apk\"", tempDir); log.Info("AM2RWrapper rebuilt into " + profile.Name + ".apk."); progress.Report(84); // Debug-sign APK CrossPlatformOperations.RunJavaJar("\"" + uberPath + "\" -a \"" + profile.Name + ".apk\"", tempDir); // Extra file cleanup File.Copy(tempDir + "/" + profile.Name + "-aligned-debugSigned.apk", CrossPlatformOperations.CURRENTPATH + "/" + profile.Name + ".apk", true); log.Info(profile.Name + ".apk signed and moved to " + CrossPlatformOperations.CURRENTPATH + "/" + profile.Name + ".apk."); HelperMethods.DeleteDirectory(tempDir); // Done progress.Report(100); log.Info("Successfully created Android APK for profile " + profile.Name + "."); CrossPlatformOperations.OpenFolderAndSelectFile(CrossPlatformOperations.CURRENTPATH + "/" + profile.Name + ".apk"); }
/// <summary> /// Installs <paramref name="profile"/>. /// </summary> /// <param name="profile"><see cref="ProfileXML"/> to be installed.</param> /// <param name="useHqMusic">Wether to patch this with high quality music or not.</param> /// <param name="progress">Provides the current progress of this method.</param> public static void InstallProfile(ProfileXML profile, bool useHqMusic, IProgress <int> progress) { log.Info("Installing profile " + profile.Name + "..."); string profilesHomePath = CrossPlatformOperations.CURRENTPATH + "/Profiles"; string profilePath = profilesHomePath + "/" + profile.Name; // Failsafe for Profiles directory if (!Directory.Exists(profilesHomePath)) { Directory.CreateDirectory(profilesHomePath); } // This failsafe should NEVER get triggered, but Miepee's broken this too much for me to trust it otherwise. if (Directory.Exists(profilePath)) { Directory.Delete(profilePath, true); } // Create profile directory Directory.CreateDirectory(profilePath); // Switch profilePath on Gtk if (OS.IsLinux) { profilePath += "/assets"; Directory.CreateDirectory(profilePath); } else if (OS.IsMac) { // Folder structure for mac is like this: // am2r.app -> Contents // -Frameworks (some libs) // -MacOS (runner) // -Resources (asset path) profilePath += "/AM2R.app/Contents"; Directory.CreateDirectory(profilePath); Directory.CreateDirectory(profilePath + "/MacOS"); Directory.CreateDirectory(profilePath + "/Resources"); profilePath += "/Resources"; log.Info("ProfileInstallation: Created folder structure."); } // Extract 1.1 ZipFile.ExtractToDirectory(CrossPlatformOperations.CURRENTPATH + "/AM2R_11.zip", profilePath); // Extracted 1.1 progress.Report(33); log.Info("Profile folder created and AM2R_11.zip extracted."); // Set local datapath for installation files var dataPath = CrossPlatformOperations.CURRENTPATH + profile.DataPath; string datawin = null, exe = null; if (OS.IsWindows) { datawin = "data.win"; exe = "AM2R.exe"; } else if (OS.IsLinux) { datawin = "game.unx"; // Use the exe name based on the desktop file in the appimage, rather than hardcoding it. string desktopContents = File.ReadAllText(CrossPlatformOperations.CURRENTPATH + "/PatchData/data/AM2R.AppDir/AM2R.desktop"); exe = Regex.Match(desktopContents, @"(?<=Exec=).*").Value; log.Info("According to AppImage desktop file, using \"" + exe + "\" as game name."); } else if (OS.IsMac) { datawin = "game.ios"; exe = "Mac_Runner"; } else { log.Error(OS.Name + " does not have valid runner / data.win names!"); } log.Info("Attempting to patch in " + profilePath); if (OS.IsWindows) { // Patch game executable if (profile.UsesYYC) { CrossPlatformOperations.ApplyXdeltaPatch(profilePath + "/data.win", dataPath + "/AM2R.xdelta", profilePath + "/" + exe); // Delete 1.1's data.win, we don't need it anymore! File.Delete(profilePath + "/data.win"); } else { CrossPlatformOperations.ApplyXdeltaPatch(profilePath + "/data.win", dataPath + "/data.xdelta", profilePath + "/" + datawin); CrossPlatformOperations.ApplyXdeltaPatch(profilePath + "/AM2R.exe", dataPath + "/AM2R.xdelta", profilePath + "/" + exe); } } else if (OS.IsUnix) // YYC and VM look exactly the same on Linux and Mac so we're all good here. { CrossPlatformOperations.ApplyXdeltaPatch(profilePath + "/data.win", dataPath + "/game.xdelta", profilePath + "/" + datawin); CrossPlatformOperations.ApplyXdeltaPatch(profilePath + "/AM2R.exe", dataPath + "/AM2R.xdelta", profilePath + "/" + exe); // Just in case the resulting file isn't chmoddded... Process.Start("chmod", "+x \"" + profilePath + "/" + exe + "\"")?.WaitForExit(); // These are not needed by linux or Mac at all, so we delete them File.Delete(profilePath + "/data.win"); File.Delete(profilePath + "/AM2R.exe"); File.Delete(profilePath + "/D3DX9_43.dll"); // Move exe one directory out on Linux, move to MacOS folder instead on Mac if (OS.IsLinux) { File.Move(profilePath + "/" + exe, profilePath.Substring(0, profilePath.LastIndexOf("/")) + "/" + exe); } else { File.Move(profilePath + "/" + exe, profilePath.Replace("Resources", "MacOS") + "/" + exe); } } else { log.Error(OS.Name + " does not have patching methods!"); } // Applied patch if (OS.IsWindows || OS.IsMac) { progress.Report(66); } else if (OS.IsLinux) { progress.Report(44); // Linux will take a bit longer, due to appimage creation } log.Info("xdelta patch(es) applied."); // Install new datafiles HelperMethods.DirectoryCopy(dataPath + "/files_to_copy", profilePath); // HQ music if (!profile.UsesCustomMusic && useHqMusic) { HelperMethods.DirectoryCopy(CrossPlatformOperations.CURRENTPATH + "/PatchData/data/HDR_HQ_in-game_music", profilePath); } // Linux post-process if (OS.IsLinux) { string assetsPath = profilePath; profilePath = profilePath.Substring(0, profilePath.LastIndexOf("/")); // Rename all songs to lowercase foreach (var file in new DirectoryInfo(assetsPath).GetFiles()) { if (file.Name.EndsWith(".ogg") && !File.Exists(file.DirectoryName + "/" + file.Name.ToLower())) { File.Move(file.FullName, file.DirectoryName + "/" + file.Name.ToLower()); } } // Copy AppImage template to here HelperMethods.DirectoryCopy(CrossPlatformOperations.CURRENTPATH + "/PatchData/data/AM2R.AppDir", profilePath + "/AM2R.AppDir/"); // Safety checks, in case the folders don't exist Directory.CreateDirectory(profilePath + "/AM2R.AppDir/usr/bin/"); Directory.CreateDirectory(profilePath + "/AM2R.AppDir/usr/bin/assets/"); // Copy game assets to the appimageDir HelperMethods.DirectoryCopy(assetsPath, profilePath + "/AM2R.AppDir/usr/bin/assets/"); File.Copy(profilePath + "/" + exe, profilePath + "/AM2R.AppDir/usr/bin/" + exe); progress.Report(66); log.Info("Gtk-specific formatting finished."); // Temp save the currentWorkingDirectory and console.error, change it to profilePath and null, call the script, and change it back. string workingDir = Directory.GetCurrentDirectory(); TextWriter cliError = Console.Error; Directory.SetCurrentDirectory(profilePath); Console.SetError(new StreamWriter(Stream.Null)); Environment.SetEnvironmentVariable("ARCH", "x86_64"); Process.Start(CrossPlatformOperations.CURRENTPATH + "/PatchData/utilities/appimagetool-x86_64.AppImage", "-n AM2R.AppDir")?.WaitForExit(); Directory.SetCurrentDirectory(workingDir); Console.SetError(cliError); // Clean files Directory.Delete(profilePath + "/AM2R.AppDir", true); Directory.Delete(assetsPath, true); File.Delete(profilePath + "/" + exe); if (File.Exists(profilePath + "/AM2R.AppImage")) { File.Delete(profilePath + "/AM2R.AppImage"); } File.Move(profilePath + "/" + "AM2R-x86_64.AppImage", profilePath + "/AM2R.AppImage"); } // Mac post-process else if (OS.IsMac) { // Rename all songs to lowercase foreach (var file in new DirectoryInfo(profilePath).GetFiles()) { if (file.Name.EndsWith(".ogg") && !File.Exists(file.DirectoryName + "/" + file.Name.ToLower())) { File.Move(file.FullName, file.DirectoryName + "/" + file.Name.ToLower()); } } // Loading custom fonts crashes on Mac, so we delete those if they exist if (Directory.Exists(profilePath + "/lang/fonts")) { Directory.Delete(profilePath + "/lang/fonts", true); } // Move Frameworks, Info.plist and PkgInfo over HelperMethods.DirectoryCopy(CrossPlatformOperations.CURRENTPATH + "/PatchData/data/Frameworks", profilePath.Replace("Resources", "Frameworks")); File.Copy(dataPath + "/Info.plist", profilePath.Replace("Resources", "") + "/Info.plist", true); File.Copy(CrossPlatformOperations.CURRENTPATH + "/PatchData/data/PkgInfo", profilePath.Replace("Resources", "") + "/PkgInfo", true); //Put profilePath back to what it was before profilePath = profilesHomePath + "/" + profile.Name; } // Copy profile.xml so we can grab data to compare for updates later! // tldr; check if we're in PatchData or not if (new DirectoryInfo(dataPath).Parent?.Name == "PatchData") { File.Copy(dataPath + "/../profile.xml", profilePath + "/profile.xml"); } else { File.Copy(dataPath + "/profile.xml", profilePath + "/profile.xml"); } // Installed datafiles progress.Report(100); log.Info("Successfully installed profile " + profile.Name + "."); }