From a14589baaf344233e537f7c43a31b837433861e6 Mon Sep 17 00:00:00 2001 From: Miepee <38186597+Miepee@users.noreply.github.com> Date: Tue, 28 Dec 2021 12:33:30 +0100 Subject: [PATCH] introduce am2rlauncherdata env var, use xdg env vars, minor cleanup - Config file on linux is not hardcoded to `~/.config/AM2RLauncher` anymore, and will now respect `$XDG_CONFIG_HOME` - Data folder on linux is not hardcoded to `~/.local/share/AM2RLauncher` anymore. Instead it will now listen for `$AM2RLAUNCHERDATA` (on win too!) and respect `$XDG_DATA_HOME` Profile Logging has been kept as is, until I figure out if AM2R on linux respects `$XDG_CONFIG_HOME`. --- .../AM2RLauncher/CrossPlatformOperations.cs | 122 ++++++++++++++++-- AM2RLauncher/AM2RLauncher/LauncherUpdater.cs | 1 - .../AM2RLauncher/MainForm/MainForm.Events.cs | 26 ++-- .../AM2RLauncher/MainForm/MainForm.Methods.cs | 15 ++- README.md | 13 +- 5 files changed, 144 insertions(+), 33 deletions(-) diff --git a/AM2RLauncher/AM2RLauncher/CrossPlatformOperations.cs b/AM2RLauncher/AM2RLauncher/CrossPlatformOperations.cs index f8a8360..fbd3b52 100644 --- a/AM2RLauncher/AM2RLauncher/CrossPlatformOperations.cs +++ b/AM2RLauncher/AM2RLauncher/CrossPlatformOperations.cs @@ -8,6 +8,7 @@ using System.IO; using System.Text; using System.Reflection; using System.Text.RegularExpressions; +using log4net; namespace AM2RLauncher { @@ -16,15 +17,20 @@ namespace AM2RLauncher /// class CrossPlatformOperations { + /// + /// The logger for , used to write any caught exceptions. + /// + private static readonly ILog log = LogManager.GetLogger(typeof(MainForm)); + /// /// Gets the current platform. /// readonly private static Platform currentPlatform = Platform.Instance; /// - /// Current Path where the Launcher is located. For Linux this is redirected to ~/.local/share/AM2RLauncher, in order to be more compliant to the XDG Base Directory Specification. + /// Current Path where the Launcher is located. For more info, check . /// - static readonly public string CURRENTPATH = currentPlatform.IsWinForms ? Path.GetDirectoryName(Assembly.GetEntryAssembly().Location) : Environment.GetEnvironmentVariable("HOME") + "/.local/share/AM2RLauncher"; + static readonly public string CURRENTPATH = GenerateCurrentPath(); /// /// Name of the Launcher executable. @@ -75,10 +81,11 @@ namespace AM2RLauncher } if (currentPlatform.IsGtk) { - //config for nix systems will be saved in .config/AM2RLauncher + //config for nix systems will be saved in XDG_CONFIG_HOME/AM2RLauncher (or if empty, ~/.config) string homePath = Environment.GetEnvironmentVariable("HOME"); - string launcherConfigPath = homePath + "/.config/AM2RLauncher/"; - string launcherConfigFilePath = launcherConfigPath + "config.xml"; + string xdgConfigHome = Environment.GetEnvironmentVariable("XDG_CONFIG_HOME"); + string launcherConfigPath = (String.IsNullOrWhiteSpace(xdgConfigHome) ? (homePath + "/.config") : xdgConfigHome) + "/AM2RLauncher"; + string launcherConfigFilePath = launcherConfigPath + "/config.xml"; XML.LauncherConfigXML launcherConfig = new XML.LauncherConfigXML(); //if folder doesn't exist, create it and the config file @@ -122,10 +129,11 @@ namespace AM2RLauncher } else if (currentPlatform.IsGtk) { - //config for nix systems will be saved in .config/AM2RLauncher + //config for nix systems will be saved in XDG_CONFIG_HOME/AM2RLauncher (or if empty, ~/.config) string homePath = Environment.GetEnvironmentVariable("HOME"); - string launcherConfigPath = homePath + "/.config/AM2RLauncher/"; - string launcherConfigFilePath = launcherConfigPath + "config.xml"; + string xdgConfigHome = Environment.GetEnvironmentVariable("XDG_CONFIG_HOME"); + string launcherConfigPath = (String.IsNullOrWhiteSpace(xdgConfigHome) ? (homePath + "/.config") : xdgConfigHome) + "/AM2RLauncher"; + string launcherConfigFilePath = launcherConfigPath + "/config.xml"; XML.LauncherConfigXML launcherConfig = new XML.LauncherConfigXML(); //if folder doesn't exist, create it and the config file @@ -170,9 +178,11 @@ namespace AM2RLauncher } else if(currentPlatform.IsGtk) { + //config for nix systems will be saved in XDG_CONFIG_HOME/AM2RLauncher (or if empty, ~/.config) string homePath = Environment.GetEnvironmentVariable("HOME"); - string launcherConfigPath = homePath + "/.config/AM2RLauncher/"; - string launcherConfigFilePath = launcherConfigPath + "config.xml"; + string xdgConfigHome = Environment.GetEnvironmentVariable("XDG_CONFIG_HOME"); + string launcherConfigPath = (String.IsNullOrWhiteSpace(xdgConfigHome) ? (homePath + "/.config") : xdgConfigHome) + "/AM2RLauncher"; + string launcherConfigFilePath = launcherConfigPath + "/config.xml"; XML.LauncherConfigXML launcherConfig = new XML.LauncherConfigXML(); //for some reason deserializing and saving back again works, not exactly sure why, but I'll take it @@ -200,9 +210,14 @@ namespace AM2RLauncher public static void OpenFolder(string path) { // We have to replace forward slashes with backslashes here on windows because explorer.exe is picky... - string realPath = currentPlatform.IsWinForms ? Environment.ExpandEnvironmentVariables(path).Replace("/", "\\") : path.Replace("~", Environment.GetEnvironmentVariable("HOME")); + // And on Nix systems, we want to replace ~ with its corresponding env var + string realPath = currentPlatform.IsWinForms ? Environment.ExpandEnvironmentVariables(path).Replace("/", "\\") + : path.Replace("~", Environment.GetEnvironmentVariable("HOME")); if (!Directory.Exists(realPath)) + { + log.Info(realPath + " did not exist and was created"); Directory.CreateDirectory(realPath); + } //needs quotes otherwise paths with space wont open if (currentPlatform.IsWinForms) @@ -214,15 +229,21 @@ namespace AM2RLauncher } /// - /// Opens and selects it in a file explorer. + /// Opens and selects it in a file explorer. + /// Only selects on Windows, on Linux it just opens the folder. Does nothing if file doesn't exist. /// /// Path to open. public static void OpenFolderAndSelectFile(string path) { // We have to replace forward slashes with backslashes here on windows because explorer.exe is picky... - string realPath = currentPlatform.IsWinForms ? Environment.ExpandEnvironmentVariables(path).Replace("/", "\\") : path.Replace("~", Environment.GetEnvironmentVariable("HOME")); + // And on nix systems, we want to replace ~ with its corresponding env var + string realPath = currentPlatform.IsWinForms ? Environment.ExpandEnvironmentVariables(path).Replace("/", "\\") + : path.Replace("~", Environment.GetEnvironmentVariable("HOME")); if (!File.Exists(realPath)) + { + log.Error(realPath + "did not exist, operation to open its folder was cancelled!"); return; + } //needs quotes otherwise paths with spaces wont open if (currentPlatform.IsWinForms) @@ -326,7 +347,8 @@ namespace AM2RLauncher if (original == output) output = output += "_"; - string arguments = "-f -d -s \"" + original.Replace(CURRENTPATH + "/","") + "\" \"" + patch.Replace(CURRENTPATH + "/", "") + "\" \"" + output.Replace(CURRENTPATH + "/", "") + "\""; + string arguments = "-f -d -s \"" + original.Replace(CURRENTPATH + "/","") + "\" \"" + patch.Replace(CURRENTPATH + "/", "") + + "\" \"" + output.Replace(CURRENTPATH + "/", "") + "\""; if (currentPlatform.IsWinForms) { @@ -368,5 +390,77 @@ namespace AM2RLauncher File.Move(output, originalOutput); } } + + /// + /// Figures out what the AM2RLauncher's should be.
+ /// Determination is as follows: + /// + /// AM2RLAUNCHERDATA environment variable is read and folders are recursively generated. + /// The current OS is checked. For Windows, the path where the executable is located will be returned.
+ /// For Linux, $XDG_DATA_HOME/AM2RLauncher will be returned. + /// Should $XDG_DATA_HOME be empty, it will default to $HOME/.local/share.
+ /// The path where the executable is located will be returned. + ///
+ /// Should any errors occur, it falls down to the next step. + ///
+ /// + private static string GenerateCurrentPath() + { + // First, we check if the user has a custom AM2RLAUNCHERDATA env var + string am2rLauncherDataEnvVar = Environment.GetEnvironmentVariable("AM2RLAUNCHERDATA"); + if (!String.IsNullOrWhiteSpace(am2rLauncherDataEnvVar)) + { + try + { + // This will create the directories recursively if they don't exist + Directory.CreateDirectory(am2rLauncherDataEnvVar); + + // Our env var is now set and directories exist + log.Info("CurrentPath is set to " + am2rLauncherDataEnvVar); + return am2rLauncherDataEnvVar; + } + catch (Exception ex) + { + log.Error($"There was an error with '{am2rLauncherDataEnvVar}'!\n{ex.Message} {ex.StackTrace}. Falling back to defaults."); + } + } + + if (currentPlatform.IsWinForms) + { + log.Info("Using default Windows CurrentPath."); + // Windows has the path where the exe is located as default + return Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); + } + else if (currentPlatform.IsGtk) + { + // First check if XDG_DATA_HOME is set, if not we'll use ~/.local/share + string xdgDataHome = Environment.GetEnvironmentVariable("XDG_DATA_HOME"); + if (string.IsNullOrWhiteSpace(xdgDataHome)) + { + log.Info("Using default Linux CurrentPath."); + xdgDataHome = Environment.GetEnvironmentVariable("HOME") + "/.local/share"; + } + + // Add AM2RLauncher to the end of the dataPath + xdgDataHome += "/AM2RLauncher"; + + try + { + // This will create the directories recursively if they don't exist + Directory.CreateDirectory(xdgDataHome); + + // Our env var is now set and directories exist + log.Info("CurrentPath is set to " + xdgDataHome); + return xdgDataHome; + } + catch (Exception ex) + { + log.Error($"There was an error with '{xdgDataHome}'!\n{ex.Message} {ex.StackTrace}. Falling back to defaults."); + } + } + + log.Info("Something went wrong, falling back to the default CurrentPath."); + return Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); + } } } diff --git a/AM2RLauncher/AM2RLauncher/LauncherUpdater.cs b/AM2RLauncher/AM2RLauncher/LauncherUpdater.cs index f3b789e..a16e86a 100644 --- a/AM2RLauncher/AM2RLauncher/LauncherUpdater.cs +++ b/AM2RLauncher/AM2RLauncher/LauncherUpdater.cs @@ -180,7 +180,6 @@ namespace AM2RLauncher // for windows, the actual application is in "AM2RLauncher.dll". Which means, we need to update the lib folder as well. if (currentPlatform.IsWinForms && Directory.Exists(CrossPlatformOperations.CURRENTPATH + "/lib")) { - // Directory.Move(CrossPlatformOperations.CURRENTPATH + "/lib", CrossPlatformOperations.CURRENTPATH + "/oldLib"); // So, because Windows behavior is dumb... // Rename all files in lib to *.bak diff --git a/AM2RLauncher/AM2RLauncher/MainForm/MainForm.Events.cs b/AM2RLauncher/AM2RLauncher/MainForm/MainForm.Events.cs index bd5de57..d0179e3 100644 --- a/AM2RLauncher/AM2RLauncher/MainForm/MainForm.Events.cs +++ b/AM2RLauncher/AM2RLauncher/MainForm/MainForm.Events.cs @@ -577,12 +577,13 @@ namespace AM2RLauncher { string currentOS = ""; if (Platform.IsWinForms) currentOS = "Windows"; - else if (Platform.IsGtk) currentOS = "Linux"; ///teeeeechnically, windows users and macos users could run GTK applications as well, so this would prolly need clarification. + else if (Platform.IsGtk) currentOS = "Linux"; ///teeeeechnically, any OS could run GTK applications as well but it'd break a lot and is thus unsupported. log.Error("Mod is for " + profile.OperatingSystem + " while current OS is " + Platform + ". Cancelling mod import."); - MessageBox.Show(Language.Text.ModIsForWrongOS.Replace("$NAME",profile.Name).Replace("$OS",profile.OperatingSystem).Replace("$CURRENTOS",currentOS), Language.Text.ErrorWindowTitle, MessageBoxType.Error); + MessageBox.Show(Language.Text.ModIsForWrongOS.Replace("$NAME",profile.Name).Replace("$OS",profile.OperatingSystem).Replace("$CURRENTOS",currentOS), + Language.Text.ErrorWindowTitle, MessageBoxType.Error); HelperMethods.DeleteDirectory(modsDir + "/" + extractedName); return; } @@ -621,7 +622,8 @@ namespace AM2RLauncher { if (IsProfileIndexValid()) { - log.Info("User opened the profile directory for profile " + profileList[settingsProfileDropDown.SelectedIndex].Name + ", which is " + profileList[settingsProfileDropDown.SelectedIndex].SaveLocation); + log.Info("User opened the profile directory for profile " + profileList[settingsProfileDropDown.SelectedIndex].Name + + ", which is " + profileList[settingsProfileDropDown.SelectedIndex].SaveLocation); CrossPlatformOperations.OpenFolder(CrossPlatformOperations.CURRENTPATH + "/Profiles/" + profileList[settingsProfileDropDown.SelectedIndex].Name); } } @@ -758,7 +760,8 @@ namespace AM2RLauncher profileVersionLabel.Text = Language.Text.VersionLabel + " " + profileList[profileDropDown.SelectedIndex].Version; CrossPlatformOperations.WriteToConfig("ProfileIndex", profileIndex + ""); //Loj, tell me a better way to do this - if (profileDropDown.SelectedIndex != 0 && (profileList[profileDropDown.SelectedIndex].SaveLocation == "%localappdata%/AM2R" || profileList[profileDropDown.SelectedIndex].SaveLocation == "default")) + if (profileDropDown.SelectedIndex != 0 && (profileList[profileDropDown.SelectedIndex].SaveLocation == "%localappdata%/AM2R" || + profileList[profileDropDown.SelectedIndex].SaveLocation == "default")) saveWarningLabel.Visible = true; else saveWarningLabel.Visible = false; @@ -798,8 +801,9 @@ namespace AM2RLauncher bool enabled = (bool)customMirrorCheck.Checked; customMirrorTextBox.Enabled = enabled; mirrorDropDown.Enabled = !enabled; + // Not sure why the dropdown menu needs this hack, but the textBox does not. if (Platform.IsWinForms) - mirrorDropDown.TextColor = mirrorDropDown.Enabled ? colGreen : colInactive; // Not sure why the dropdown menu needs this hack, but the textBox does not. + mirrorDropDown.TextColor = mirrorDropDown.Enabled ? colGreen : colInactive; mirrorLabel.TextColor = !enabled ? colGreen : colInactive; // Create warning dialog when enabling @@ -901,7 +905,8 @@ namespace AM2RLauncher ProfileXML profile = profileList[settingsProfileDropDown.SelectedIndex]; log.Info("User is attempting to delete profile " + profile.Name + "."); - DialogResult result = MessageBox.Show(Language.Text.DeleteModWarning.Replace("$NAME", profile.Name), Language.Text.WarningWindowTitle, MessageBoxButtons.OKCancel, MessageBoxType.Warning, MessageBoxDefaultButton.Cancel); + DialogResult result = MessageBox.Show(Language.Text.DeleteModWarning.Replace("$NAME", profile.Name), Language.Text.WarningWindowTitle, + MessageBoxButtons.OKCancel, MessageBoxType.Warning, MessageBoxDefaultButton.Cancel); if (result == DialogResult.Ok) { @@ -980,7 +985,8 @@ namespace AM2RLauncher if (profileList.Where(p => p.Name == profile.Name).FirstOrDefault() != null || Directory.Exists(CrossPlatformOperations.CURRENTPATH + "/Profiles/" + profile.Name)) { // Mod is already installed, so we can update! - DialogResult result = MessageBox.Show(Language.Text.UpdateModWarning.Replace("$NAME", currentProfile.Name), Language.Text.WarningWindowTitle, MessageBoxButtons.OKCancel, MessageBoxType.Warning, MessageBoxDefaultButton.Cancel); + DialogResult result = MessageBox.Show(Language.Text.UpdateModWarning.Replace("$NAME", currentProfile.Name), Language.Text.WarningWindowTitle, + MessageBoxButtons.OKCancel, MessageBoxType.Warning, MessageBoxDefaultButton.Cancel); if (result == DialogResult.Ok) { @@ -1003,7 +1009,8 @@ namespace AM2RLauncher // Cancel the operation! // Show message to tell user that mod could not be found, install this separately log.Error("Mod is not installed! Cancelling mod update."); - MessageBox.Show(Language.Text.UpdateModButtonWrongMod.Replace("$NAME", currentProfile.Name).Replace("$SELECT", profile.Name), Language.Text.WarningWindowTitle, MessageBoxButtons.OK); + MessageBox.Show(Language.Text.UpdateModButtonWrongMod.Replace("$NAME", currentProfile.Name).Replace("$SELECT", profile.Name), + Language.Text.WarningWindowTitle, MessageBoxButtons.OK); abort = true; } @@ -1042,7 +1049,8 @@ namespace AM2RLauncher if (updateState == UpdateState.Downloading) { - var result = MessageBox.Show(Language.Text.CloseOnCloningText, Language.Text.WarningWindowTitle, MessageBoxButtons.YesNo, MessageBoxType.Warning, MessageBoxDefaultButton.No); + var result = MessageBox.Show(Language.Text.CloseOnCloningText, Language.Text.WarningWindowTitle, MessageBoxButtons.YesNo, + MessageBoxType.Warning, MessageBoxDefaultButton.No); if (result == DialogResult.No) { e.Cancel = true; diff --git a/AM2RLauncher/AM2RLauncher/MainForm/MainForm.Methods.cs b/AM2RLauncher/AM2RLauncher/MainForm/MainForm.Methods.cs index eee581a..3ce346f 100644 --- a/AM2RLauncher/AM2RLauncher/MainForm/MainForm.Methods.cs +++ b/AM2RLauncher/AM2RLauncher/MainForm/MainForm.Methods.cs @@ -58,7 +58,7 @@ namespace AM2RLauncher private bool IsProfileInstalled(ProfileXML profile) { if (Platform.IsWinForms) return File.Exists(CrossPlatformOperations.CURRENTPATH + "/Profiles/" + profile.Name + "/AM2R.exe"); - if (Platform.IsGtk) return File.Exists(CrossPlatformOperations.CURRENTPATH + "/Profiles/" + profile.Name + "/AM2R.AppImage"); //should we check for .AppRun as well? + if (Platform.IsGtk) return File.Exists(CrossPlatformOperations.CURRENTPATH + "/Profiles/" + profile.Name + "/AM2R.AppImage"); return false; } @@ -304,18 +304,18 @@ namespace AM2RLauncher string datawin = null, exe = null; - if(Platform.IsWinForms) + if (Platform.IsWinForms) { datawin = "data.win"; exe = "AM2R.exe"; } - else if(Platform.IsGtk) + else if (Platform.IsGtk) { 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, use \"" + exe + "\" as game name."); + log.Info("According to AppImage desktop file, using \"" + exe + "\" as game name."); } log.Info("Attempting to patch in " + profilePath); @@ -353,7 +353,7 @@ namespace AM2RLauncher // Applied patch if (Platform.IsWinForms) UpdateProgressBar(66); - else if (Platform.IsGtk) UpdateProgressBar(44);//linux will take a bit longer, due to appimage creation + else if (Platform.IsGtk) UpdateProgressBar(44); //linux will take a bit longer, due to appimage creation log.Info("xdelta patch(es) applied."); // Install new datafiles @@ -418,7 +418,7 @@ namespace AM2RLauncher log.Info("Successfully installed profile " + profile.Name + "."); // This is just for visuals because the average windows end user will ask why it doesn't go to the end otherwise. - if(Platform.IsWinForms) + if (Platform.IsWinForms) Thread.Sleep(1000); } @@ -644,7 +644,8 @@ namespace AM2RLauncher ProfileXML profile = profileList[profileIndex.Value]; // These are used on both windows and linux for game logging - string savePath = Platform.IsWinForms ? profile.SaveLocation.Replace("%localappdata%", Environment.GetEnvironmentVariable("LOCALAPPDATA")) : profile.SaveLocation.Replace("~", Environment.GetEnvironmentVariable("HOME")); + string savePath = Platform.IsWinForms ? profile.SaveLocation.Replace("%localappdata%", Environment.GetEnvironmentVariable("LOCALAPPDATA")) + : profile.SaveLocation.Replace("~", Environment.GetEnvironmentVariable("HOME")); DirectoryInfo logDir = new DirectoryInfo(savePath + "/logs"); string date = string.Join("-", DateTime.Now.ToString().Split(Path.GetInvalidFileNameChars(), StringSplitOptions.RemoveEmptyEntries)); diff --git a/README.md b/README.md index 9c5f621..515cc9b 100644 --- a/README.md +++ b/README.md @@ -16,12 +16,15 @@ Linux needs the following dependencies installed: - `openssl` - `fuse2` +As well as these dependencies to run AM2R: +- libopenal1:i386 +- libpulse0:i386 + Optionally, for APK creation any Java runtime is needed. ### Arch Linux On Arch Linux, you can install these by running this: -(Multilib repositories are required, instructions on how to enable them can be found [here](https://wiki.archlinux.org/title/Official_repositories#Enabling_multilib)) - +(Multilib repositories are required, instructions on how to enable them can be found [here](https://wiki.archlinux.org/title/Official_repositories#Enabling_multilib)) `sudo pacman -S --needed dotnet-runtime fuse2 gtk3 libappindicator-gtk3 openssl webkit2gtk xdelta3 lib32-openal lib32-libpulse jre-openjdk` For other distros, refer to your local package manager for instructions. @@ -31,6 +34,12 @@ Downloads can be found at the [Release Page](https://github.com/AM2R-Community-D Alternatively, for Arch Linux users an [AUR Package](https://aur.archlinux.org/packages/am2rlauncher/) also exist. Install it with `makepkg -si` or use your favourite AUR helper. +## Configuration and Data Files +The AM2RLauncher stores its files in the following places: + - On Windows, it stores the config file to the `AM2RLauncher.exe.config` next to the binary, and its data files in the same folder as the binary. + - On Linux, it stores the config file to `$XDG_CONFIG_HOME/AM2RLauncher` and its data files to `$XDG_DATA_HOME/AM2RLauncher` (which are defaulting back to `~/.config` and `~/.local/share` respectively). +The AM2RLauncher data can get quite big, so if you wish to change where it stores it, you can do so with the `AM2RLAUNCHERDATA` environment variable (i.e `AM2RLAUNCHERDATA="D:\MyLauncherData"` or `AM2RLAUNCHERDATA="/mnt/bigDrive/launcherData"`). + # Compiling Instructions: ## Dependencies For compiling for Windows .Net Framework 4.8 SDK is needed. For Linux .Net Core 5.0 SDK is needed.