diff --git a/Knossos.NET/Classes/FsoBuild.cs b/Knossos.NET/Classes/FsoBuild.cs index 6a2c9f52..d6296b4e 100644 --- a/Knossos.NET/Classes/FsoBuild.cs +++ b/Knossos.NET/Classes/FsoBuild.cs @@ -275,6 +275,12 @@ public async Task RunFSO(FsoExecType executableType, string cmdline, fso.StartInfo.UseShellExecute = false; if (workingFolder != null) fso.StartInfo.WorkingDirectory = workingFolder; + if(Knossos.inPortableMode && Knossos.globalSettings.portableFsoPreferences) + { + var prefPath = Path.Combine(KnUtils.KnetFolderPath!, "kn_portable", "HardLightProductions", "FreeSpaceOpen") + Path.DirectorySeparatorChar; + Log.Add(Log.LogSeverity.Information, "FsoBuild.RunFSO()", "Used preferences path: " + prefPath); + fso.StartInfo.EnvironmentVariables.Add("FSO_PREFERENCES_PATH", prefPath); + } if (Knossos.globalSettings.envVars != "") { foreach (var envVar in Knossos.globalSettings.envVars.Split(",")) @@ -343,6 +349,10 @@ public async Task RunFSO(FsoExecType executableType, string cmdline, cmd.StartInfo.RedirectStandardInput = true; cmd.StartInfo.StandardOutputEncoding = new UTF8Encoding(false); cmd.StartInfo.WorkingDirectory = folderPath; + if (Knossos.inPortableMode && Knossos.globalSettings.portableFsoPreferences) + { + cmd.StartInfo.EnvironmentVariables.Add("FSO_PREFERENCES_PATH", Path.Combine(KnUtils.KnetFolderPath!, "kn_portable", "HardLightProductions", "FreeSpaceOpen") + Path.DirectorySeparatorChar); + } cmd.Start(); string result = cmd.StandardOutput.ReadToEnd(); output = result; diff --git a/Knossos.NET/Classes/KnUtils.cs b/Knossos.NET/Classes/KnUtils.cs index 46152247..bc41b705 100644 --- a/Knossos.NET/Classes/KnUtils.cs +++ b/Knossos.NET/Classes/KnUtils.cs @@ -88,6 +88,13 @@ public static string? KnetFolderPath { return Path.GetDirectoryName(KnUtils.AppImagePath); } + else if (IsMacOS && WasInstallerUsed()) + { + var execFullPath = Environment.ProcessPath; + var cutOff = execFullPath!.IndexOf(".app") + 4; + var realName = execFullPath![..cutOff]; + return Path.GetDirectoryName(realName); + } else { return AppDomain.CurrentDomain.BaseDirectory; @@ -112,7 +119,14 @@ public static string? KnetFolderPath /// fullpath as a string public static string GetKnossosDataFolderPath() { - return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData, Environment.SpecialFolderOption.Create), "KnossosNET"); + if (!Knossos.inPortableMode) + { + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData, Environment.SpecialFolderOption.Create), "KnossosNET"); + } + else + { + return Path.Combine(KnetFolderPath!, "kn_portable", "KnossosNET"); //If inPortableMode = true, KnetFolderPath is not null + } } /// @@ -124,20 +138,28 @@ public static string GetKnossosDataFolderPath() /// fullpath as string public static string GetFSODataFolderPath() { - if (!string.IsNullOrEmpty(fsoPrefPath)) + if (Knossos.inPortableMode && Knossos.globalSettings.portableFsoPreferences) { - return fsoPrefPath; + return Path.Combine(KnetFolderPath!, "kn_portable", "HardLightProductions", "FreeSpaceOpen"); //If inPortableMode = true, KnetFolderPath is not null } else { - if (KnUtils.isMacOS){ - return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "HardLightProductions", "FreeSpaceOpen"); + if (!string.IsNullOrEmpty(fsoPrefPath)) + { + return fsoPrefPath; } - if(IsLinux) + else { - return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "share", "HardLightProductions", "FreeSpaceOpen"); + if (KnUtils.isMacOS) + { + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "HardLightProductions", "FreeSpaceOpen"); + } + if (IsLinux) + { + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "share", "HardLightProductions", "FreeSpaceOpen"); + } + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData, Environment.SpecialFolderOption.Create), "HardLightProductions", "FreeSpaceOpen"); } - return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData, Environment.SpecialFolderOption.Create), "HardLightProductions", "FreeSpaceOpen"); } } diff --git a/Knossos.NET/Classes/Knossos.cs b/Knossos.NET/Classes/Knossos.cs index f64d5d85..42def2cc 100644 --- a/Knossos.NET/Classes/Knossos.cs +++ b/Knossos.NET/Classes/Knossos.cs @@ -29,6 +29,32 @@ public static class Knossos public static bool flagDataLoaded = false; private static object? ttsObject = null; private static bool forceUpdateDownload = false; //Only intended to test the update system! + public static bool inPortableMode { get; private set; } = false; + public static bool isKnDataFolderReadOnly { get; private set; } = false; + + /// + /// Static constructor + /// + static Knossos() + { + try + { + //We are in portable mode? if so set everything up ahead of all else + var pathToExec = KnUtils.KnetFolderPath; + if (pathToExec != null) + { + if (Directory.Exists(Path.Combine(pathToExec, "kn_portable"))) + { + inPortableMode = true; + } + } + } + catch (Exception ex) + { + //At this stage we can only log to console + Log.WriteToConsole("Knossos() - " + ex.Message); + } + } /// /// StartUp sequence @@ -58,15 +84,30 @@ public static async void StartUp(bool isQuickLaunch, bool forceUpdate) } catch (Exception ex) { + isKnDataFolderReadOnly = true; Log.Add(Log.LogSeverity.Error, "Knossos.StartUp()", ex); if (MainWindow.instance != null) { - await MessageBox.Show(MainWindow.instance, "Unable to write to KnossosNET data folder.", "KnossosNET Error", MessageBox.MessageBoxButtons.OK); + await MessageBox.Show(MainWindow.instance, "Unable to write to KnossosNET data folder:\n'"+ KnUtils.GetKnossosDataFolderPath()+"'\nSome functions may not work correctly.", "KnossosNET Error", MessageBox.MessageBoxButtons.OK); } } Log.Add(Log.LogSeverity.Information, "Knossos.StartUp()", "=== KnossosNET v" + AppVersion + " Start ==="); + if (inPortableMode) + { + Log.Add(Log.LogSeverity.Information, "Knossos.StartUp()", "Running in PORTABLE MODE."); + try + { + Directory.CreateDirectory(Path.Combine(KnUtils.KnetFolderPath!, "kn_portable", "HardLightProductions", "FreeSpaceOpen")); + Directory.CreateDirectory(Path.Combine(KnUtils.KnetFolderPath!, "kn_portable", "Library")); + } + catch (Exception ex) + { + Log.Add(Log.LogSeverity.Error, "Knossos.Startup()", ex); + } + } + //Load language files Lang.LoadFiles(); @@ -1157,7 +1198,7 @@ public static async void PlayMod(Mod mod, FsoExecType fsoExecType, bool standalo cmdline += " -mod " + modFlag; } - Log.Add(Log.LogSeverity.Information, "Knossos.PlayMod()", "Used cmdLine : " + cmdline); + if (MainWindow.instance != null && globalSettings.warnNewSettingsSystem) { @@ -1193,6 +1234,28 @@ await Dispatcher.UIThread.InvokeAsync(async () => { } } + //Portable mode and limitations in unsupported fso versions + if (inPortableMode && globalSettings.portableFsoPreferences) + { + try + { + var fsoVersion = new SemanticVersion(fsoBuild.version); + var newPortableModeVersion = new SemanticVersion("24.3.0-20241211"); + if (fsoVersion < newPortableModeVersion) + { + cmdline = "-portable_mode " + cmdline; + //older portable mode uses working path to pickup the .ini and store pilots + globalSettings.WriteFS2IniValues(Path.Combine(rootPath, "fs2_open.ini")); + } + } + catch(Exception ex) + { + Log.Add(Log.LogSeverity.Error, "Knossos.PlayMod()", ex); + } + } + + Log.Add(Log.LogSeverity.Information, "Knossos.PlayMod()", "Used cmdLine : " + cmdline); + //Launch FSO!!! var fsoResult = await fsoBuild.RunFSO(fsoExecType, cmdline, rootPath, false); diff --git a/Knossos.NET/Models/GlobalSettings.cs b/Knossos.NET/Models/GlobalSettings.cs index 33ad9309..b0e18938 100644 --- a/Knossos.NET/Models/GlobalSettings.cs +++ b/Knossos.NET/Models/GlobalSettings.cs @@ -261,6 +261,8 @@ public MainWindowViewModel.SortType sortType public string pxoLogin { get; set; } = ""; [JsonIgnore] public string pxoPassword { get; set; } = ""; + [JsonPropertyName("portable_fso_preferences")] + public bool portableFsoPreferences { get; set; } = true; /* Developer Settings */ [JsonPropertyName("no_system_cmd")] @@ -667,6 +669,7 @@ public void Load() warnNewSettingsSystem = tempSettings.warnNewSettingsSystem; mainMenuOpen = tempSettings.mainMenuOpen; sortType = tempSettings.sortType; + portableFsoPreferences = tempSettings.portableFsoPreferences; ReadFS2IniValues(); Log.Add(Log.LogSeverity.Information, "GlobalSettings.Load()", "Global settings have been loaded"); @@ -684,13 +687,19 @@ public void Load() { Log.Add(Log.LogSeverity.Error, "GlobalSettings.Load()", ex); } + if(Knossos.inPortableMode) + { + basePath = Path.Combine(KnUtils.KnetFolderPath!, "kn_portable", "Library"); + } } /// /// Save setting data to the fs2_open.ini /// Stops the ini-watcher if it was enabled and re-enables it to avoid triggering a read + /// Optional: Specific path to write the .ini to, need to be FULL PATH /// - public void WriteFS2IniValues() + /// + public void WriteFS2IniValues(string? customFullPath = null) { try { @@ -850,10 +859,20 @@ public void WriteFS2IniValues() wasWatchingIni = iniWatcher.EnableRaisingEvents; iniWatcher.EnableRaisingEvents = false; } - parser.WriteFile(KnUtils.GetFSODataFolderPath() + Path.DirectorySeparatorChar + "fs2_open.ini", data, new UTF8Encoding(false)); - if(iniWatcher!= null && wasWatchingIni) + if (customFullPath == null) + { + parser.WriteFile(KnUtils.GetFSODataFolderPath() + Path.DirectorySeparatorChar + "fs2_open.ini", data, new UTF8Encoding(false)); + Log.Add(Log.LogSeverity.Information, "GlobalSettings.WriteFS2IniValues", "Writen ini: " + KnUtils.GetFSODataFolderPath() + Path.DirectorySeparatorChar + "fs2_open.ini"); + } + else + { + + parser.WriteFile(customFullPath, data, new UTF8Encoding(false)); + Log.Add(Log.LogSeverity.Information, "GlobalSettings.WriteFS2IniValues", "Writen ini: " + customFullPath); + } + + if (iniWatcher!= null && wasWatchingIni) iniWatcher.EnableRaisingEvents = true; - Log.Add(Log.LogSeverity.Information, "GlobalSettings.WriteFS2IniValues","Writen ini: "+ KnUtils.GetFSODataFolderPath() + Path.DirectorySeparatorChar + "fs2_open.ini"); } catch (Exception ex) { diff --git a/Knossos.NET/Models/Log.cs b/Knossos.NET/Models/Log.cs index 1b09929c..5a5168e7 100644 --- a/Knossos.NET/Models/Log.cs +++ b/Knossos.NET/Models/Log.cs @@ -37,10 +37,13 @@ public static void Add(LogSeverity logSeverity, string from, string data) Task.Run(async () => { try { - await WaitForFileAccess(LogFilePath); - using (var writer = new StreamWriter(LogFilePath, true)) + if (!Knossos.isKnDataFolderReadOnly) { - writer.WriteLine(logString, Encoding.UTF8); + await WaitForFileAccess(LogFilePath); + using (var writer = new StreamWriter(LogFilePath, true)) + { + writer.WriteLine(logString, Encoding.UTF8); + } } } catch (Exception ex) diff --git a/Knossos.NET/ViewModels/GlobalSettingsViewModel.cs b/Knossos.NET/ViewModels/GlobalSettingsViewModel.cs index eedb3009..1be06637 100644 --- a/Knossos.NET/ViewModels/GlobalSettingsViewModel.cs +++ b/Knossos.NET/ViewModels/GlobalSettingsViewModel.cs @@ -36,7 +36,8 @@ public partial class GlobalSettingsViewModel : ViewModelBase private const long speed10MB = 170000000; /* For display only */ - + [ObservableProperty] + internal bool isPortableMode = false; [ObservableProperty] internal bool flagDataLoaded = false; [ObservableProperty] @@ -61,7 +62,6 @@ public partial class GlobalSettingsViewModel : ViewModelBase internal bool isAVX2 = false; /* Knossos Settings */ - [ObservableProperty] internal string basePath = string.Empty; //When this is changed settings are saved immediately. @@ -500,6 +500,14 @@ internal string EnvVars set { if (envVars != value) { this.SetProperty(ref envVars, value); UnCommitedChanges = true; } } } + /* MISC */ + private bool portableFsoPreferences = true; + internal bool PortableFsoPreferences + { + get { return portableFsoPreferences; } + set { if (portableFsoPreferences != value) { this.SetProperty(ref portableFsoPreferences, value); UnCommitedChanges = true; } } + } + internal string globalCmd = string.Empty; // In order to have hidden dev options, we need a setter for globalCMD public string GlobalCmd @@ -522,6 +530,7 @@ public string GlobalCmd public GlobalSettingsViewModel() { + isPortableMode = Knossos.inPortableMode; } /// @@ -1037,6 +1046,9 @@ public void LoadData() //Multi Port MultiPort = Knossos.globalSettings.multiPort; + //MISC + PortableFsoPreferences = Knossos.globalSettings.portableFsoPreferences; + UnCommitedChanges = false; } @@ -1364,6 +1376,9 @@ internal void SaveCommand() //Multi port Knossos.globalSettings.multiPort = MultiPort; + //MISC + Knossos.globalSettings.portableFsoPreferences = PortableFsoPreferences; + Knossos.globalSettings.Save(); UnCommitedChanges = false; } diff --git a/Knossos.NET/ViewModels/Windows/QuickSetupViewModel.cs b/Knossos.NET/ViewModels/Windows/QuickSetupViewModel.cs index 516f47a9..9f28b63f 100644 --- a/Knossos.NET/ViewModels/Windows/QuickSetupViewModel.cs +++ b/Knossos.NET/ViewModels/Windows/QuickSetupViewModel.cs @@ -23,6 +23,9 @@ public partial class QuickSetupViewModel : ViewModelBase [ObservableProperty] internal bool lastPage = false; + [ObservableProperty] + internal bool isPortableMode = false; + [ObservableProperty] internal string? libraryPath = null; @@ -49,12 +52,14 @@ public partial class QuickSetupViewModel : ViewModelBase public static QuickSetupViewModel? Instance; public QuickSetupViewModel() - { + { + isPortableMode = Knossos.inPortableMode; } public QuickSetupViewModel(Window dialog) { this.dialog = dialog; + isPortableMode = Knossos.inPortableMode; Instance = this; UpdateBuildName(MainWindowViewModel.Instance!.LatestStable); TrackRepoStatus(); diff --git a/Knossos.NET/Views/GlobalSettingsView.axaml b/Knossos.NET/Views/GlobalSettingsView.axaml index f5a68262..5addb297 100644 --- a/Knossos.NET/Views/GlobalSettingsView.axaml +++ b/Knossos.NET/Views/GlobalSettingsView.axaml @@ -28,7 +28,7 @@ Library Folder - + @@ -369,6 +369,11 @@ Polish + + + + + Detected Build Settings diff --git a/Knossos.NET/Views/Windows/QuickSetupView.axaml b/Knossos.NET/Views/Windows/QuickSetupView.axaml index 886a9826..d540c57b 100644 --- a/Knossos.NET/Views/Windows/QuickSetupView.axaml +++ b/Knossos.NET/Views/Windows/QuickSetupView.axaml @@ -35,12 +35,21 @@ - Setting up the library folder - First, you must set a library folder. Go to the "Settings" tab and under the "Knossos" section click on the "Browse" button and choose or create a folder for Knossos to use. It is highly recommended to set this as an empty folder in a location with a large amount of available storage. Once you have set the folder be sure to click the "Save" button in the upper right corner of the "Settings" tab. - The Knossos library folder is where all game and mod data will be saved to. Make sure you always have space available on this drive before installing a new mod or update. - Current Library Folder - - Go to the "Settings" tab and set the library folder to continue. + + Setting up the library folder + First, you must set a library folder. Go to the "Settings" tab and under the "Knossos" section click on the "Browse" button and choose or create a folder for Knossos to use. It is highly recommended to set this as an empty folder in a location with a large amount of available storage. Once you have set the folder be sure to click the "Save" button in the upper right corner of the "Settings" tab. + The Knossos library folder is where all game and mod data will be saved to. Make sure you always have space available on this drive before installing a new mod or update. + Current Library Folder + + Go to the "Settings" tab and set the library folder to continue. + + + Knet is running in portable mode + This means Knet settings and FSO pilots, settings and data are saved inside the 'kn_portable' folder, you can not set a library folder in this mode. + If you ever need to stop using the portable mode move, rename or delete the 'kn_portable' folder + Current Library Folder + +