/// <summary> /// Deletes a profile from the Mods and Profiles folder. /// </summary> /// <param name="profile">The profile to delete.</param> public static void DeleteProfile(ProfileXML profile) { log.Info("Attempting to delete profile " + profile.Name + "..."); // Delete folder in Mods if (Directory.Exists(CrossPlatformOperations.CURRENTPATH + profile.DataPath)) { HelperMethods.DeleteDirectory(CrossPlatformOperations.CURRENTPATH + profile.DataPath); } // Delete the zip file in Mods if (File.Exists(CrossPlatformOperations.CURRENTPATH + profile.DataPath + ".zip")) { // For some reason, it was set at read only, so we undo that here File.SetAttributes(CrossPlatformOperations.CURRENTPATH + profile.DataPath + ".zip", FileAttributes.Normal); File.Delete(CrossPlatformOperations.CURRENTPATH + profile.DataPath + ".zip"); } // Delete folder in Profiles if (Directory.Exists(CrossPlatformOperations.CURRENTPATH + "/Profiles/" + profile.Name)) { HelperMethods.DeleteDirectory(CrossPlatformOperations.CURRENTPATH + "/Profiles/" + profile.Name); } log.Info("Successfully deleted profile " + profile.Name + "."); }
internal static ProfileXML ToProfileXML(this Profile profile) { ProfileXML profileXML = new ProfileXML() { Description = profile.Description, Email = profile.Email, Fonts = profile.Fonts, Name = profile.Name, OperatingSystem = profile.OperatingSystem, CPU = profile.CPU, Battery = profile.Battery, EnableAudioApi = profile.EnableAudioApi, EnablePlugins = profile.EnablePlugins, EnableMediaPlugins = profile.EnableMediaPlugins, RandomTimersEnabled = profile.RandomTimersEnabled, UserAgent = profile.UserAgent, Screen = profile.Screen, HistoryLength = profile.HistoryLength, WebGL = profile.WebGL, FakeClientRects = profile.FakeClientRects, Canvas = profile.Canvas, EnableNetwork = profile.EnableNetwork, Language = profile.Language, GeoIpEnabled = profile.GeoIpEnabled, ByPassProxySites = profile.ByPassProxySites, Proxies = profile.Proxies, }; return(profileXML); }
/// <summary> /// This opens the save directory for the current profile. /// </summary> private void SaveButtonClickEvent(object sender, EventArgs e) { if (!IsProfileIndexValid()) { return; } ProfileXML profile = profileList[modSettingsProfileDropDown.SelectedIndex]; log.Info("User opened the save directory for profile " + profile.Name + ", which is " + profile.SaveLocation); CrossPlatformOperations.OpenFolder(profile.SaveLocation); }
/// <summary> /// Archives a given Profile by making a copy with "Name (version)". Does silently nothing if user archives already exist /// </summary> /// <param name="profile">The profile to archive</param> public static void ArchiveProfile(ProfileXML profile) { // temporarily serialize and deserialize to essentially "clone" the variable as otherwise we'd modify references File.WriteAllText(Path.GetTempPath() + "/" + profile.Name, Serializer.Serialize <ProfileXML>(profile)); profile = Serializer.Deserialize <ProfileXML>(File.ReadAllText(Path.GetTempPath() + "/" + profile.Name)); string originalName = profile.Name; // Change name to include version and be unique profile.Name += " (" + profile.Version + ")"; // if we're archiving community updates, remove the "latest" part profile.Name = profile.Name.Replace("Community Updates Latest", "Community Updates"); log.Info("Archiving " + profile.Name); string profileArchivePath = CrossPlatformOperations.CURRENTPATH + "/Profiles/" + profile.Name; // Do NOT overwrite if a path with this name already exists! It is likely an existing user archive. if (!Directory.Exists(profileArchivePath)) { // Rename current profile if we have it installed if (Directory.Exists(CrossPlatformOperations.CURRENTPATH + "/Profiles/" + originalName)) { Directory.Move(CrossPlatformOperations.CURRENTPATH + "/Profiles/" + originalName, profileArchivePath); } // Set as non-installable so that it's just treated as a launching reference profile.Installable = false; string modArchivePath = CrossPlatformOperations.CURRENTPATH + "/Mods/" + profile.Name; // Do NOT overwrite if a path with this name already exists! It is likely an existing user archive. if (!Directory.Exists(modArchivePath)) { Directory.CreateDirectory(modArchivePath); File.WriteAllText(modArchivePath + "/profile.xml", Serializer.Serialize <ProfileXML>(profile)); log.Info("Finished archival."); } else { HelperMethods.DeleteDirectory(profileArchivePath); log.Info("Cancelling archival! User-defined archive in Mods already exists."); } } // If our desired rename already exists, it's probably a user archive... so we just delete the original folder and move on with installation of the new version. else { HelperMethods.DeleteDirectory(CrossPlatformOperations.CURRENTPATH + "/Profiles/" + originalName); log.Info("Cancelling archival! User-defined archive in Profiles already exists."); } }
/// <summary> /// Scans the PatchData and Mods folders for valid profile entries, creates and returns a list of them. /// </summary> /// <returns>A <see cref="List{ProfileXML}"/> containing all valid profile entries.</returns> public static List <ProfileXML> LoadProfiles() { log.Info("Loading profiles..."); List <ProfileXML> profileList = new List <ProfileXML>(); // Check for and add the Community Updates profile if (File.Exists(CrossPlatformOperations.CURRENTPATH + "/PatchData/profile.xml")) { ProfileXML profile = Serializer.Deserialize <ProfileXML>(File.ReadAllText(CrossPlatformOperations.CURRENTPATH + "/PatchData/profile.xml")); profile.DataPath = "/PatchData/data"; profileList.Add(profile); } // Safety check to generate the Mods folder if it does not exist if (!Directory.Exists(CrossPlatformOperations.CURRENTPATH + "/Mods")) { Directory.CreateDirectory(CrossPlatformOperations.CURRENTPATH + "/Mods"); } // Get Mods folder info DirectoryInfo modsDir = new DirectoryInfo(CrossPlatformOperations.CURRENTPATH + "/Mods"); // Add all extracted profiles in Mods to the profileList. foreach (DirectoryInfo dir in modsDir.GetDirectories()) { // If no profile.xml exists we don't add anything if (!File.Exists(dir.FullName + "/profile.xml")) { continue; } ProfileXML prof = Serializer.Deserialize <ProfileXML>(File.ReadAllText(dir.FullName + "/profile.xml")); // Safety check for non-installable profiles if (prof.Installable || IsProfileInstalled(prof)) { prof.DataPath = "/Mods/" + dir.Name; profileList.Add(prof); } // If not installable and isn't installed, remove it else if (!IsProfileInstalled(prof)) { prof.DataPath = "/Mods/" + dir.Name; DeleteProfile(prof); } } log.Info("Loaded " + profileList.Count + " profile(s)."); return(profileList); }
private void UpdateAutomationProfile(ProfileXML savedProfile) { try { profile.Description = savedProfile.Description; profile.Email = savedProfile.Email; profile.Fonts = savedProfile.Fonts; profile.Name = savedProfile.Name; profile.OperatingSystem = savedProfile.OperatingSystem; profile.CPU.DeviceMemory = savedProfile.CPU.DeviceMemory; profile.CPU.HardwareConcurrency = savedProfile.CPU.HardwareConcurrency; profile.Battery.Charging = savedProfile.Battery.Charging; profile.Battery.ChargingTime = savedProfile.Battery.ChargingTime; profile.Battery.DischargingTime = savedProfile.Battery.DischargingTime; profile.Battery.Level = savedProfile.Battery.Level; profile.EnableAudioApi = savedProfile.EnableAudioApi; profile.EnablePlugins = savedProfile.EnablePlugins; profile.EnableMediaPlugins = savedProfile.EnableMediaPlugins; profile.RandomTimersEnabled = savedProfile.RandomTimersEnabled; profile.UserAgent = savedProfile.UserAgent; profile.Screen.Color = savedProfile.Screen.Color; profile.Screen.Height = savedProfile.Screen.Height; profile.Screen.Width = savedProfile.Screen.Width; profile.HistoryLength = savedProfile.HistoryLength; profile.WebGL.BrowserplugsR = savedProfile.WebGL.BrowserplugsR; profile.WebGL.Plus1 = savedProfile.WebGL.Plus1; profile.WebGL.Plus2 = savedProfile.WebGL.Plus2; profile.WebGL.Plus3 = savedProfile.WebGL.Plus3; profile.WebGL.Plus4 = savedProfile.WebGL.Plus4; profile.WebGL.Plus5 = savedProfile.WebGL.Plus5; profile.FakeClientRects = savedProfile.FakeClientRects; profile.Canvas.B = savedProfile.Canvas.B; profile.Canvas.G = savedProfile.Canvas.G; profile.Canvas.R = savedProfile.Canvas.R; profile.EnableNetwork = savedProfile.EnableNetwork; profile.Language = savedProfile.Language; profile.GeoIpEnabled = savedProfile.GeoIpEnabled; profile.ByPassProxySites = savedProfile.ByPassProxySites; profile.Proxies = savedProfile.Proxies; } catch (Exception ex) { throw new Exception("Update backup profile error"); } }
/// <summary> /// Checks if <paramref name="profile"/> is installed. /// </summary> /// <param name="profile">The <see cref="ProfileXML"/> that should be checked for installation.</param> /// <returns><see langword="true"/> if yes, <see langword="false"/> if not.</returns> public static bool IsProfileInstalled(ProfileXML profile) { if (OS.IsWindows) { return(File.Exists(CrossPlatformOperations.CURRENTPATH + "/Profiles/" + profile.Name + "/AM2R.exe")); } if (OS.IsLinux) { return(File.Exists(CrossPlatformOperations.CURRENTPATH + "/Profiles/" + profile.Name + "/AM2R.AppImage")); } if (OS.IsMac) { return(Directory.Exists(CrossPlatformOperations.CURRENTPATH + "/Profiles/" + profile.Name + "/AM2R.app")); } log.Error(OS.Name + " can't have profiles installed!"); return(false); }
/// <summary> /// Gets called when <see cref="deleteModButton"/> gets clicked. Deletes the current selected <see cref="ProfileXML"/> in <see cref="modSettingsProfileDropDown"/>. /// </summary> private void DeleteModButtonClicked(object sender, EventArgs e) { ProfileXML profile = profileList[modSettingsProfileDropDown.SelectedIndex]; log.Info("User is attempting to delete profile " + profile.Name + "."); DialogResult result = MessageBox.Show(this, HelperMethods.GetText(Text.DeleteModWarning, profile.Name), Text.WarningWindowTitle, MessageBoxButtons.OKCancel, MessageBoxType.Warning, MessageBoxDefaultButton.Cancel); // if user didn't press ok, cancel if (result != DialogResult.Ok) { log.Info("User has cancelled profile deletion."); return; } log.Info("User did not cancel. Proceeding to delete " + profile); DeleteProfileAndAdjustLists(profile); log.Info(profile + " has been deleted"); MessageBox.Show(this, HelperMethods.GetText(Text.DeleteModButtonSuccess, profile.Name), Text.SuccessWindowTitle); }
/// <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> /// Runs the Game, works cross platform. /// </summary> public static void RunGame(ProfileXML profile, bool useLogging, string envVars = "") { // These are used on both windows and linux for game logging string savePath = OS.IsWindows ? profile.SaveLocation.Replace("%localappdata%", Environment.GetEnvironmentVariable("LOCALAPPDATA")) : profile.SaveLocation.Replace("~", CrossPlatformOperations.NIXHOME); DirectoryInfo logDir = new DirectoryInfo(savePath + "/logs"); string date = String.Join("-", DateTime.Now.ToString().Split(Path.GetInvalidFileNameChars(), StringSplitOptions.RemoveEmptyEntries)); log.Info("Launching game profile " + profile.Name + "."); if (OS.IsWindows) { // Sets the arguments to empty, or to the profiles save path/logs and create time based logs. Creates the folder if necessary. string arguments = ""; // Game logging if (useLogging) { log.Info("Performing logging setup for profile " + profile.Name + "."); if (!Directory.Exists(logDir.FullName)) { Directory.CreateDirectory(logDir.FullName); } if (File.Exists(logDir.FullName + "/" + profile.Name + ".txt")) { HelperMethods.RecursiveRollover(logDir.FullName + "/" + profile.Name + ".txt", 5); } StreamWriter stream = File.AppendText(logDir.FullName + "/" + profile.Name + ".txt"); stream.WriteLine("AM2RLauncher " + Core.VERSION + " log generated at " + date); if (Core.IsThisRunningFromWine) { stream.WriteLine("Using WINE!"); } stream.Flush(); stream.Close(); arguments = "-debugoutput \"" + logDir.FullName + "/" + profile.Name + ".txt\" -output \"" + logDir.FullName + "/" + profile.Name + ".txt\""; } ProcessStartInfo proc = new ProcessStartInfo(); proc.WorkingDirectory = CrossPlatformOperations.CURRENTPATH + "/Profiles/" + profile.Name; proc.FileName = proc.WorkingDirectory + "/AM2R.exe"; proc.Arguments = arguments; log.Info("CWD of Profile is " + proc.WorkingDirectory); using var p = Process.Start(proc); Core.SetForegroundWindow(p.MainWindowHandle); p.WaitForExit(); } else if (OS.IsLinux) { ProcessStartInfo startInfo = new ProcessStartInfo(); log.Info("User does " + (String.IsNullOrWhiteSpace(envVars) ? "not" : "") + " have custom environment variables set."); //TODO: make this more readable at one day if (!String.IsNullOrWhiteSpace(envVars)) { for (int i = 0; i < envVars.Count(f => f == '='); i++) { // Env var variable string variable = envVars.Substring(0, envVars.IndexOf('=')); envVars = envVars.Replace(variable + "=", ""); // This thing here is the value parser. Since values are sometimes in quotes, i need to compensate for them. int valueSubstringLength; if (envVars[0] != '"') // If value is not embedded in "", check if there are spaces left. If yes, get the index of the space, if not that was the last { if (envVars.IndexOf(' ') >= 0) { valueSubstringLength = envVars.IndexOf(' ') + 1; } else { valueSubstringLength = envVars.Length; } } else // If value is embedded in "", check if there are spaces after the "". if yes, get index of that, if not that was the last { int secondQuoteIndex = envVars.IndexOf('"', envVars.IndexOf('"') + 1); if (envVars.IndexOf(' ', secondQuoteIndex) >= 0) { valueSubstringLength = envVars.IndexOf(' ', secondQuoteIndex) + 1; } else { valueSubstringLength = envVars.Length; } } // Env var value string value = envVars.Substring(0, valueSubstringLength); envVars = envVars.Substring(value.Length); log.Info("Adding user variable \"" + variable + "\" with value \"" + value + "\""); startInfo.EnvironmentVariables[variable] = value; } } // If we're supposed to log profiles, add events that track those and append them to this var. otherwise keep it null string terminalOutput = null; startInfo.UseShellExecute = false; startInfo.WorkingDirectory = CrossPlatformOperations.CURRENTPATH + "/Profiles/" + profile.Name; startInfo.FileName = startInfo.WorkingDirectory + "/AM2R.AppImage"; log.Info("CWD of Profile is " + startInfo.WorkingDirectory); log.Debug("Launching game with following variables: "); foreach (System.Collections.DictionaryEntry item in startInfo.EnvironmentVariables) { log.Debug("Key: \"" + item.Key + "\" Value: \"" + item.Value + "\""); } using (Process p = new Process()) { p.StartInfo = startInfo; if (useLogging) { p.StartInfo.RedirectStandardOutput = true; p.OutputDataReceived += (_, e) => { terminalOutput += e.Data + "\n"; }; p.StartInfo.RedirectStandardError = true; p.ErrorDataReceived += (_, e) => { terminalOutput += e.Data + "\n"; }; } p.Start(); if (useLogging) { p.BeginOutputReadLine(); p.BeginErrorReadLine(); } p.WaitForExit(); } if (terminalOutput != null) { log.Info("Performed logging setup for profile " + profile.Name + "."); if (!Directory.Exists(logDir.FullName)) { Directory.CreateDirectory(logDir.FullName); } if (File.Exists(logDir.FullName + "/" + profile.Name + ".txt")) { HelperMethods.RecursiveRollover(logDir.FullName + "/" + profile.Name + ".txt", 5); } StreamWriter stream = File.AppendText(logDir.FullName + "/" + profile.Name + ".txt"); // Write general info stream.WriteLine("AM2RLauncher " + Core.VERSION + " log generated at " + date); // Write what was in the terminal stream.WriteLine(terminalOutput); stream.Flush(); stream.Close(); } } else if (OS.IsMac) { // Sets the arguments to only open the game, or append the profiles save path/logs and create time based logs. Creates the folder if necessary. string arguments = "AM2R.app -W"; // Game logging if (useLogging) { log.Info("Performing logging setup for profile " + profile.Name + "."); if (!Directory.Exists(logDir.FullName)) { Directory.CreateDirectory(logDir.FullName); } if (File.Exists(logDir.FullName + "/" + profile.Name + ".txt")) { HelperMethods.RecursiveRollover(logDir.FullName + "/" + profile.Name + ".txt", 5); } StreamWriter stream = File.AppendText(logDir.FullName + "/" + profile.Name + ".txt"); stream.WriteLine("AM2RLauncher " + Core.VERSION + " log generated at " + date); stream.Flush(); stream.Close(); arguments += " --stdout \"" + logDir.FullName + "/" + profile.Name + ".txt\" --stderr \"" + logDir.FullName + "/" + profile.Name + ".txt\""; } ProcessStartInfo proc = new ProcessStartInfo(); proc.WorkingDirectory = CrossPlatformOperations.CURRENTPATH + "/Profiles/" + profile.Name; proc.FileName = "open"; proc.Arguments = arguments; log.Info("CWD of Profile is " + proc.WorkingDirectory); using var p = Process.Start(proc); p?.WaitForExit(); } else { log.Error(OS.Name + " cannot run games!"); } log.Info("Profile " + profile.Name + " process exited."); }
/// <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 + "."); }
/// <summary> /// Deletes a profile and reloads the necessary UI components. /// </summary> /// <param name="profile">The profile to delete.</param> private void DeleteProfileAndAdjustLists(ProfileXML profile) { Profile.DeleteProfile(profile); LoadProfilesAndAdjustLists(); }
private void ArchiveProfileAndAdjustLists(ProfileXML profile) { Profile.ArchiveProfile(profile); LoadProfilesAndAdjustLists(); }
/// <summary> /// Does a bunch of stuff, depending on the current state of <see cref="updateState"/>. /// </summary> private async void PlayButtonClickEvent(object sender, EventArgs e) { // State Check UpdateStateMachine(); // Check if 1.1 is installed by forcing invalidation Profile.Is11Installed(true); switch (updateState) { #region Download case PlayButtonState.Download: log.Info("Attempting to clone repository " + currentMirror + "..."); bool successful = true; // Update playButton states SetPlayButtonState(PlayButtonState.Downloading); // Enable progressBar progressBar.Visible = true; progressLabel.Visible = true; progressBar.Value = 0; // Set up progressBar update method var c = new CloneOptions { OnTransferProgress = TransferProgressHandlerMethod }; // Everything after this is on a different thread, so the rest of the launcher isn't locked up. try { if (Directory.Exists(CrossPlatformOperations.CURRENTPATH + "/PatchData")) { log.Info("PatchData directory already exists, cleaning up..."); HelperMethods.DeleteDirectory(CrossPlatformOperations.CURRENTPATH + "/PatchData"); } await Task.Run(() => Repository.Clone(currentMirror, CrossPlatformOperations.CURRENTPATH + "/PatchData", c)); } catch (UserCancelledException) { // We deliberately cancelled this! successful = false; } catch (LibGit2SharpException ex) // This is for any exceptions from libgit { // Libgit2sharp error messages are always in english! if (ex.Message.ToLower().Contains("failed to send request") || ex.Message.ToLower().Contains("connection with the server was terminated") || ex.Message.ToLower().Contains("failed to resolve address")) { log.Error("Internet connection dropped while attempting to clone repository" + currentMirror + "!"); MessageBox.Show(this, Text.InternetConnectionDrop, Text.WarningWindowTitle, MessageBoxType.Warning); } else { log.Error("LibGit2SharpException: " + ex.Message + "\n*****Stack Trace*****\n\n" + ex.StackTrace); MessageBox.Show(this, ex.Message + "\n*****Stack Trace*****\n\n" + ex.StackTrace, Text.ErrorWindowTitle, MessageBoxType.Error); if (Directory.Exists(CrossPlatformOperations.CURRENTPATH + "/PatchData")) { HelperMethods.DeleteDirectory(CrossPlatformOperations.CURRENTPATH + "/PatchData"); } } successful = false; } catch (Exception ex) // This is if somehow any other exception might get thrown as well. { log.Error(ex.Message + "\n*****Stack Trace*****\n\n" + ex.StackTrace); MessageBox.Show(this, ex.Message + "\n*****Stack Trace*****\n\n" + ex.StackTrace, Text.ErrorWindowTitle, MessageBoxType.Error); if (Directory.Exists(CrossPlatformOperations.CURRENTPATH + " / PatchData")) { HelperMethods.DeleteDirectory(CrossPlatformOperations.CURRENTPATH + "/PatchData"); } successful = false; } log.Info("Repository clone attempt finished " + (successful ? "successfully." : "unsuccessfully.")); currentGitObject = 0; // Reset progressBar after clone is finished progressLabel.Visible = false; progressLabel.Text = ""; progressBar.Visible = false; progressBar.Value = 0; // Just need to switch this to anything that isn't an "active" state so SetUpdateState() actually does something SetPlayButtonState(PlayButtonState.Install); // This needs to be run BEFORE the state check so that the Mod Settings tab doesn't weird out LoadProfilesAndAdjustLists(); // Do a state check UpdateStateMachine(); break; #endregion #region Downloading case PlayButtonState.Downloading: var result = MessageBox.Show(this, Text.CloseOnCloningText, Text.WarningWindowTitle, MessageBoxButtons.YesNo, MessageBoxType.Warning, MessageBoxDefaultButton.No); if (result != DialogResult.Yes) { return; } log.Info("User cancelled download!"); isGitProcessGettingCancelled = true; // We don't need to delete any folders here, the cancelled gitClone will do that automatically for us :) // But we should probably wait a bit before proceeding, since cleanup can take a while Thread.Sleep(1000); isGitProcessGettingCancelled = false; break; #endregion #region Select11 case PlayButtonState.Select11: log.Info("Requesting user input for AM2R_11.zip..."); OpenFileDialog fileFinder = GetSingleZipDialog(Text.Select11FileDialog); if (fileFinder.ShowDialog(this) != DialogResult.Ok) { log.Info("User cancelled the selection."); return; } // Default filename is whitespace if (String.IsNullOrWhiteSpace(fileFinder.FileName)) { log.Error("User did not supply valid input. Cancelling import."); return; } // If either a directory was selected or the file somehow went missing, cancel if (!File.Exists(fileFinder.FileName)) { log.Error("Selected AM2R_11.zip file not found! Cancelling import."); break; } IsZipAM2R11ReturnCodes errorCode = Profile.CheckIfZipIsAM2R11(fileFinder.FileName); if (errorCode != IsZipAM2R11ReturnCodes.Successful) { log.Error("User tried to input invalid AM2R_11.zip file (" + errorCode + "). Cancelling import."); MessageBox.Show(this, Text.ZipIsNotAM2R11 + "\n\nError Code: " + errorCode, Text.ErrorWindowTitle, MessageBoxType.Error); return; } // We check if it exists first, because someone coughDRUIDcough might've copied it into here while on the showDialog if (fileFinder.FileName != CrossPlatformOperations.CURRENTPATH + "/AM2R_11.zip") { File.Copy(fileFinder.FileName, CrossPlatformOperations.CURRENTPATH + "/AM2R_11.zip"); } log.Info("AM2R_11.zip successfully imported."); UpdateStateMachine(); break; #endregion #region Install case PlayButtonState.Install: progressBar.Visible = true; progressBar.Value = 0; SetPlayButtonState(PlayButtonState.Installing); // Make sure the main interface state machines properly UpdateApkState(); UpdateProfileState(); // If the file cannot be launched due to anti-virus shenanigans or any other reason, we try catch here try { // Check if xdelta is installed on unix and exit if not if (OS.IsUnix && !CrossPlatformOperations.CheckIfXdeltaIsInstalled()) { MessageBox.Show(this, Text.XdeltaNotFound, Text.WarningWindowTitle, MessageBoxButtons.OK); SetPlayButtonState(PlayButtonState.Install); UpdateStateMachine(); log.Error("Xdelta not found. Aborting installing a profile..."); return; } var progressIndicator = new Progress <int>(UpdateProgressBar); bool useHqMusic = hqMusicPCCheck.Checked.Value; await Task.Run(() => Profile.InstallProfile(profileList[profileIndex.Value], useHqMusic, progressIndicator)); // This is just for visuals because the average windows end user will ask why it doesn't go to the end otherwise. if (OS.IsWindows) { Thread.Sleep(500); } } catch (Exception ex) { log.Error(ex.Message + "\n*****Stack Trace*****\n\n" + ex.StackTrace); MessageBox.Show(this, ex.Message + "\n*****Stack Trace*****\n\n" + ex.StackTrace, Text.ErrorWindowTitle, MessageBoxType.Error); } progressBar.Visible = false; progressBar.Value = 0; // Just need to switch this to anything that isn't an "active" state so SetUpdateState() actually does something SetPlayButtonState(PlayButtonState.Play); UpdateStateMachine(); break; #endregion #region Play case PlayButtonState.Play: if (!IsProfileIndexValid()) { return; } ProfileXML profile = profileList[profileIndex.Value]; Visible = false; SetPlayButtonState(PlayButtonState.Playing); // Make sure the main interface state machines properly UpdateApkState(); UpdateProfileState(); this.ShowInTaskbar = false; trayIndicator.Visible = true; WindowState windowStateBeforeLaunching = this.WindowState; Minimize(); string envVarText = customEnvVarTextBox?.Text; bool createDebugLogs = profileDebugLogCheck.Checked.Value; await Task.Run(() => Profile.RunGame(profile, createDebugLogs, envVarText)); this.ShowInTaskbar = true; trayIndicator.Visible = false; Show(); BringToFront(); Visible = true; WindowState = windowStateBeforeLaunching; SetPlayButtonState(PlayButtonState.Play); UpdateStateMachine(); break; #endregion default: throw new NotImplementedException("Encountered invalid update state: " + updateState + "!"); } }
/// <summary> /// After the <see cref="playButton"/> has bee loaded, git pull if a repo has been cloned already. /// </summary> private async void PlayButtonLoadComplete(object sender, EventArgs e) { LoadProfilesAndAdjustLists(); if (!Profile.IsPatchDataCloned() || !(bool)autoUpdateAM2RCheck.Checked) { return; } SetPlayButtonState(PlayButtonState.Downloading); progressBar.Visible = true; progressLabel.Visible = true; progressBar.Value = 0; // Try to pull first. try { log.Info("Attempting to pull repository " + currentMirror + "..."); await Task.Run(() => Profile.PullPatchData(TransferProgressHandlerMethod)); // Thank you druid, for this case that should never happen if (!File.Exists(CrossPlatformOperations.CURRENTPATH + "/PatchData/profile.xml")) { log.Error("Druid PatchData corruption occurred!"); await Application.Instance.InvokeAsync(() => { MessageBox.Show(this, Text.CorruptPatchData, Text.ErrorWindowTitle, MessageBoxType.Error); }); HelperMethods.DeleteDirectory(CrossPlatformOperations.CURRENTPATH + "/PatchData"); return; } } catch (UserCancelledException ex) { log.Info(ex.Message); MessageBox.Show(this, Text.CorruptPatchData, Text.ErrorWindowTitle, MessageBoxType.Error); HelperMethods.DeleteDirectory(CrossPlatformOperations.CURRENTPATH + "/PatchData"); } catch (LibGit2SharpException ex) // This is for any exceptions from libgit { // Libgit2sharp error messages are always in english! if (ex.Message.ToLower().Contains("failed to send request") || ex.Message.ToLower().Contains("connection with the server was terminated") || ex.Message.ToLower().Contains("failed to resolve address")) { if (!(bool)autoUpdateAM2RCheck.Checked) { log.Error("Internet connection failed while attempting to pull repository" + currentMirror + "!"); MessageBox.Show(this, Text.InternetConnectionDrop, Text.WarningWindowTitle, MessageBoxType.Warning); } } else { log.Error(ex.Message + "\n*****Stack Trace*****\n\n" + ex.StackTrace); MessageBox.Show(this, ex.Message + "\n*****Stack Trace*****\n\n" + ex.StackTrace, Text.ErrorWindowTitle, MessageBoxType.Error); } } catch (Exception ex) // This is if somehow any other exception might get thrown as well. { log.Error(ex.Message + "\n*****Stack Trace*****\n\n" + ex.StackTrace); MessageBox.Show(this, ex.Message + "\n*****Stack Trace*****\n\n" + ex.StackTrace, Text.ErrorWindowTitle, MessageBoxType.Error); } finally { progressBar.Visible = false; progressLabel.Visible = false; LoadProfilesAndAdjustLists(); } // Handling for updates - if current version does not match PatchData version, rename folder so that we attempt to install! // Also, add a non-installable profile for it so people can access the older version or delete it from the mod manager. if (profileList.Count > 0 && Profile.IsProfileInstalled(profileList[0])) { ProfileXML currentXML = Serializer.Deserialize <ProfileXML>(File.ReadAllText(CrossPlatformOperations.CURRENTPATH + "/Profiles/Community Updates (Latest)/profile.xml")); if (currentXML.Version != profileList[0].Version) { log.Info("New game version (" + profileList[0].Version + ") detected! Beginning archival of version " + currentXML.Version + "..."); Profile.ArchiveProfile(currentXML); profileDropDown.SelectedIndex = 0; LoadProfilesAndAdjustLists(); } } SetPlayButtonState(PlayButtonState.Install); UpdateStateMachine(); }
/// <summary> /// Gets called, when <see cref="updateModButton"/> gets clicked. Opens a window, so the user can select a zip, which will be updated over /// the current selected <see cref="ProfileXML"/> in <see cref="modSettingsProfileDropDown"/>. /// </summary> private void UpdateModButtonClicked(object sender, EventArgs e) { log.Info("User requested to update mod. Requesting user input for new mod .zip..."); ProfileXML currentProfile = profileList[modSettingsProfileDropDown.SelectedIndex]; OpenFileDialog fileFinder = GetSingleZipDialog(Text.SelectModFileDialog); // If user didn't click OK, cancel if (fileFinder.ShowDialog(this) != DialogResult.Ok) { log.Info("User cancelled the Mod selection."); return; } log.Info("User selected \"" + fileFinder.FileName + "\""); // If either a directory was selected, no file was selected or the file somehow went missing, cancel if (!File.Exists(fileFinder.FileName)) { log.Error("Selected mod .zip file not found! Cancelling mod update."); return; } //TODO: move most of this into AM2RLauncher.Profile? FileInfo modFile = new FileInfo(fileFinder.FileName); string modsDir = new DirectoryInfo(CrossPlatformOperations.CURRENTPATH + "/Mods").FullName; string extractedName = Path.GetFileNameWithoutExtension(modFile.Name) + "_new"; string extractedModDir = modsDir + "/" + extractedName; // If for some reason old files remain, delete them so that extraction doesnt throw if (Directory.Exists(extractedModDir)) { Directory.Delete(extractedModDir, true); } // Directory doesn't exist -> extract! ZipFile.ExtractToDirectory(fileFinder.FileName, extractedModDir); // If mod doesn't have a profile.xml, throw an error and cleanup if (!File.Exists(extractedModDir + "/profile.xml")) { log.Error(fileFinder.FileName + " does not contain profile.xml! Cancelling mod update."); MessageBox.Show(this, HelperMethods.GetText(Text.ModIsInvalidMessage, extractedName), Text.ErrorWindowTitle, MessageBoxType.Error); Directory.Delete(extractedModDir, true); return; } // Check by *name*, if the mod was installed already ProfileXML profile = Serializer.Deserialize <ProfileXML>(File.ReadAllText(extractedModDir + "/profile.xml")); // If the selected mod is not installed, tell user that they should add it and cleanup if (profileList.FirstOrDefault(p => p.Name == profile.Name) == null) { log.Error("Mod is not installed! Cancelling mod update."); MessageBox.Show(this, HelperMethods.GetText(Text.UpdateModButtonWrongMod, currentProfile.Name).Replace("$SELECT", profile.Name), Text.WarningWindowTitle, MessageBoxButtons.OK); HelperMethods.DeleteDirectory(extractedModDir); return; } // If user doesn't want to update, cleanup DialogResult updateResult = MessageBox.Show(this, HelperMethods.GetText(Text.UpdateModWarning, currentProfile.Name), Text.WarningWindowTitle, MessageBoxButtons.OKCancel, MessageBoxType.Warning, MessageBoxDefaultButton.Cancel); if (updateResult != DialogResult.Ok) { log.Error("User has cancelled mod update!"); HelperMethods.DeleteDirectory(extractedModDir); return; } // If the profile isn't installed, don't ask about archiving it if (Profile.IsProfileInstalled(currentProfile)) { DialogResult archiveResult = MessageBox.Show(this, HelperMethods.GetText(Text.ArchiveMod, currentProfile.Name + " " + Text.VersionLabel + currentProfile.Version), Text.WarningWindowTitle, MessageBoxButtons.YesNo, MessageBoxType.Warning, MessageBoxDefaultButton.No); // User wants to archive profile if (archiveResult == DialogResult.Yes) { ArchiveProfileAndAdjustLists(currentProfile); } } DeleteProfileAndAdjustLists(currentProfile); // Rename directory to take the old one's place string originalFolder = modsDir + "/" + Path.GetFileNameWithoutExtension(modFile.Name); Directory.Move(extractedModDir, originalFolder); // Adjust our lists so it gets recognized LoadProfilesAndAdjustLists(); modSettingsProfileDropDown.SelectedIndex = profileList.FindIndex(p => p.Name == currentProfile.Name); if (modSettingsProfileDropDown.SelectedIndex == -1) { modSettingsProfileDropDown.SelectedIndex = 0; } log.Info("Successfully updated mod profile " + profile.Name + "."); MessageBox.Show(this, HelperMethods.GetText(Text.ModSuccessfullyInstalledMessage, currentProfile.Name), Text.SuccessWindowTitle); }
/// <summary> /// Runs when <see cref="addModButton"/> is clicked. Brings up a file select to select a mod, and adds that to the mod directory. /// </summary> private void AddModButtonClicked(object sender, EventArgs e) { log.Info("User requested to add mod. Requesting user input for new mod .zip..."); OpenFileDialog fileFinder = GetSingleZipDialog(Text.SelectModFileDialog); // If user didn't press ok, cancel if (fileFinder.ShowDialog(this) != DialogResult.Ok) { log.Info("User cancelled the Mod selection."); return; } log.Info("User selected \"" + fileFinder.FileName + "\""); // If either a directory was selected, user pressed OK without selecting anything or the file somehow went missing, cancel if (!File.Exists(fileFinder.FileName)) { log.Error("Selected mod .zip file not found! Cancelling import."); return; } //TODO: move most of this into AM2RLauncher.Profile? FileInfo modFile = new FileInfo(fileFinder.FileName); string modsDir = new DirectoryInfo(CrossPlatformOperations.CURRENTPATH + "/Mods").FullName; string modFileName = Path.GetFileNameWithoutExtension(modFile.Name); string extractedModDir = modsDir + "/" + modFileName; // Check first, if the directory is already there, if yes, throw error if (Directory.Exists(extractedModDir)) { string existingProfileName = Serializer.Deserialize <ProfileXML>(File.ReadAllText(extractedModDir + "/profile.xml")).Name; log.Error("Mod is already imported as " + modFileName + "! Cancelling mod import."); MessageBox.Show(this, HelperMethods.GetText(Text.ModIsAlreadyInstalledMessage, existingProfileName), Text.WarningWindowTitle, MessageBoxType.Warning); return; } // Directory doesn't exist -> extract! ZipFile.ExtractToDirectory(modFile.FullName, extractedModDir); log.Info("Imported and extracted mod .zip as " + modFileName); // If profile.xml doesn't exist, throw an error and cleanup if (!File.Exists(extractedModDir + "/profile.xml")) { log.Error(modFile.Name + " does not contain profile.xml! Cancelling mod import."); MessageBox.Show(this, HelperMethods.GetText(Text.ModIsInvalidMessage, modFileName), Text.ErrorWindowTitle, MessageBoxType.Error); Directory.Delete(extractedModDir, true); return; } ProfileXML profile = Serializer.Deserialize <ProfileXML>(File.ReadAllText(extractedModDir + "/profile.xml")); // If OS versions mismatch, throw error and cleanup if (OS.Name != profile.OperatingSystem) { log.Error("Mod is for " + profile.OperatingSystem + " while current OS is " + OS.Name + ". Cancelling mod import."); MessageBox.Show(this, HelperMethods.GetText(Text.ModIsForWrongOS, profile.Name).Replace("$OS", profile.OperatingSystem).Replace("$CURRENTOS", OS.Name), Text.ErrorWindowTitle, MessageBoxType.Error); HelperMethods.DeleteDirectory(extractedModDir); return; } // If mod was installed/added by *name* already, throw error and cleanup if (profileList.FirstOrDefault(p => p.Name == profile.Name) != null) { log.Error(profile.Name + " is already installed."); MessageBox.Show(this, HelperMethods.GetText(Text.ModIsAlreadyInstalledMessage, profile.Name), Text.WarningWindowTitle, MessageBoxType.Warning); HelperMethods.DeleteDirectory(extractedModDir); return; } // Reload list so mod gets recognized LoadProfilesAndAdjustLists(); // Adjust profileIndex to point to newly added mod. if its not found for whatever reason, we default to first community updates modSettingsProfileDropDown.SelectedIndex = profileList.FindIndex(p => p.Name == profile.Name); if (modSettingsProfileDropDown.SelectedIndex == -1) { modSettingsProfileDropDown.SelectedIndex = 0; } log.Info(profile.Name + " successfully added."); MessageBox.Show(this, HelperMethods.GetText(Text.ModSuccessfullyInstalledMessage, profile.Name), Text.SuccessWindowTitle); }