diff --git a/src/XIVLauncher.Common.Unix/Compatibility/CompatibilityTools.cs b/src/XIVLauncher.Common.Unix/Compatibility/CompatibilityTools.cs index 8216526f..28d92c00 100644 --- a/src/XIVLauncher.Common.Unix/Compatibility/CompatibilityTools.cs +++ b/src/XIVLauncher.Common.Unix/Compatibility/CompatibilityTools.cs @@ -1,72 +1,53 @@ using System; using System.Collections.Generic; -using System.Collections.Specialized; using System.Diagnostics; using System.IO; using System.Linq; -using System.Net.Http; using System.Threading.Tasks; -using Serilog; +using System.Net.Http; using XIVLauncher.Common.Util; - -#if FLATPAK -#warning THIS IS A FLATPAK BUILD!!! -#endif +using Serilog; namespace XIVLauncher.Common.Unix.Compatibility; public class CompatibilityTools { - private DirectoryInfo toolDirectory; + private DirectoryInfo wineDirectory; + private DirectoryInfo dxvkDirectory; private StreamWriter logWriter; -#if WINE_XIV_ARCH_LINUX - private const string WINE_XIV_RELEASE_URL = "https://github.com/goatcorp/wine-xiv-git/releases/download/8.5.r4.g4211bac7/wine-xiv-staging-fsync-git-arch-8.5.r4.g4211bac7.tar.xz"; -#elif WINE_XIV_FEDORA_LINUX - private const string WINE_XIV_RELEASE_URL = "https://github.com/goatcorp/wine-xiv-git/releases/download/8.5.r4.g4211bac7/wine-xiv-staging-fsync-git-fedora-8.5.r4.g4211bac7.tar.xz"; -#else - private const string WINE_XIV_RELEASE_URL = "https://github.com/goatcorp/wine-xiv-git/releases/download/8.5.r4.g4211bac7/wine-xiv-staging-fsync-git-ubuntu-8.5.r4.g4211bac7.tar.xz"; -#endif - private const string WINE_XIV_RELEASE_NAME = "wine-xiv-staging-fsync-git-8.5.r4.g4211bac7"; - public bool IsToolReady { get; private set; } public WineSettings Settings { get; private set; } - private string WineBinPath => Settings.StartupType == WineStartupType.Managed ? - Path.Combine(toolDirectory.FullName, WINE_XIV_RELEASE_NAME, "bin") : - Settings.CustomBinPath; - private string Wine64Path => Path.Combine(WineBinPath, "wine64"); - private string WineServerPath => Path.Combine(WineBinPath, "wineserver"); - - public bool IsToolDownloaded => File.Exists(Wine64Path) && Settings.Prefix.Exists; + public DxvkSettings DxvkSettings { get; private set; } - private readonly Dxvk.DxvkHudType hudType; + public bool IsToolDownloaded => File.Exists(Settings.WinePath) && Settings.Prefix.Exists; + private readonly bool gamemodeOn; - private readonly string dxvkAsyncOn; - public CompatibilityTools(WineSettings wineSettings, Dxvk.DxvkHudType hudType, bool? gamemodeOn, bool? dxvkAsyncOn, DirectoryInfo toolsFolder) + private Dictionary extraEnvironmentVars; + + public CompatibilityTools(WineSettings wineSettings, DxvkSettings dxvkSettings, bool? gamemodeOn, DirectoryInfo toolsFolder, Dictionary extraEnvVars = null) { this.Settings = wineSettings; - this.hudType = hudType; + this.DxvkSettings = dxvkSettings; this.gamemodeOn = gamemodeOn ?? false; - this.dxvkAsyncOn = (dxvkAsyncOn ?? false) ? "1" : "0"; - this.toolDirectory = new DirectoryInfo(Path.Combine(toolsFolder.FullName, "beta")); + this.extraEnvironmentVars = extraEnvVars ?? new Dictionary(); + + this.wineDirectory = new DirectoryInfo(Path.Combine(toolsFolder.FullName, "wine")); this.dxvkDirectory = new DirectoryInfo(Path.Combine(toolsFolder.FullName, "dxvk")); this.logWriter = new StreamWriter(wineSettings.LogFile.FullName); - if (wineSettings.StartupType == WineStartupType.Managed) - { - if (!this.toolDirectory.Exists) - this.toolDirectory.Create(); + if (!this.wineDirectory.Exists) + this.wineDirectory.Create(); - if (!this.dxvkDirectory.Exists) - this.dxvkDirectory.Create(); - } + if (!this.dxvkDirectory.Exists) + this.dxvkDirectory.Create(); if (!wineSettings.Prefix.Exists) wineSettings.Prefix.Create(); @@ -74,30 +55,60 @@ public CompatibilityTools(WineSettings wineSettings, Dxvk.DxvkHudType hudType, b public async Task EnsureTool(DirectoryInfo tempPath) { - if (!File.Exists(Wine64Path)) + if (!File.Exists(Settings.WinePath)) { - Log.Information("Compatibility tool does not exist, downloading"); - await DownloadTool(tempPath).ConfigureAwait(false); + Log.Information($"Compatibility tool does not exist, downloading {Settings.DownloadUrl}"); + await DownloadTool(wineDirectory, Settings.DownloadUrl).ConfigureAwait(false); } - EnsurePrefix(); - await Dxvk.InstallDxvk(Settings.Prefix, dxvkDirectory).ConfigureAwait(false); + + if (DxvkSettings.Enabled) + await InstallDxvk().ConfigureAwait(false); IsToolReady = true; } - private async Task DownloadTool(DirectoryInfo tempPath) + private async Task InstallDxvk() { - using var client = new HttpClient(); - var tempFilePath = Path.Combine(tempPath.FullName, $"{Guid.NewGuid()}"); + var dxvkPath = Path.Combine(dxvkDirectory.FullName, DxvkSettings.FolderName, "x64"); + if (!Directory.Exists(dxvkPath)) + { + Log.Information($"DXVK does not exist, downloading {DxvkSettings.DownloadUrl}"); + await DownloadTool(dxvkDirectory, DxvkSettings.DownloadUrl).ConfigureAwait(false); + } - await File.WriteAllBytesAsync(tempFilePath, await client.GetByteArrayAsync(WINE_XIV_RELEASE_URL).ConfigureAwait(false)).ConfigureAwait(false); + var system32 = Path.Combine(Settings.Prefix.FullName, "drive_c", "windows", "system32"); + var files = Directory.GetFiles(dxvkPath); - PlatformHelpers.Untar(tempFilePath, this.toolDirectory.FullName); + foreach (string fileName in files) + { + File.Copy(fileName, Path.Combine(system32, Path.GetFileName(fileName)), true); + } + + // 32-bit files for Directx9. + var dxvkPath32 = Path.Combine(dxvkDirectory.FullName, DxvkSettings.FolderName, "x32"); + var syswow64 = Path.Combine(Settings.Prefix.FullName, "drive_c", "windows", "syswow64"); + + if (Directory.Exists(dxvkPath32)) + { + files = Directory.GetFiles(dxvkPath32); + + foreach (string fileName in files) + { + File.Copy(fileName, Path.Combine(syswow64, Path.GetFileName(fileName)), true); + } + } + } + + private async Task DownloadTool(DirectoryInfo installDirectory, string downloadUrl) + { + using var client = new HttpClient(); + var tempPath = Path.GetTempFileName(); - Log.Information("Compatibility tool successfully extracted to {Path}", this.toolDirectory.FullName); + File.WriteAllBytes(tempPath, await client.GetByteArrayAsync(downloadUrl)); + PlatformHelpers.Untar(tempPath, installDirectory.FullName); - File.Delete(tempFilePath); + File.Delete(tempPath); } private void ResetPrefix() @@ -118,7 +129,7 @@ public void EnsurePrefix() public Process RunInPrefix(string command, string workingDirectory = "", IDictionary environment = null, bool redirectOutput = false, bool writeLog = false, bool wineD3D = false) { - var psi = new ProcessStartInfo(Wine64Path); + var psi = new ProcessStartInfo(Settings.WinePath); psi.Arguments = command; Log.Verbose("Running in prefix: {FileName} {Arguments}", psi.FileName, command); @@ -127,7 +138,7 @@ public Process RunInPrefix(string command, string workingDirectory = "", IDictio public Process RunInPrefix(string[] args, string workingDirectory = "", IDictionary environment = null, bool redirectOutput = false, bool writeLog = false, bool wineD3D = false) { - var psi = new ProcessStartInfo(Wine64Path); + var psi = new ProcessStartInfo(Settings.WinePath); foreach (var arg in args) psi.ArgumentList.Add(arg); @@ -135,7 +146,7 @@ public Process RunInPrefix(string[] args, string workingDirectory = "", IDiction return RunInPrefix(psi, workingDirectory, environment, redirectOutput, writeLog, wineD3D); } - private void MergeDictionaries(StringDictionary a, IDictionary b) + private void MergeDictionaries(IDictionary a, IDictionary b) { if (b is null) return; @@ -143,12 +154,34 @@ private void MergeDictionaries(StringDictionary a, IDictionary b foreach (var keyValuePair in b) { if (a.ContainsKey(keyValuePair.Key)) - a[keyValuePair.Key] = keyValuePair.Value; + { + if (keyValuePair.Key == "LD_PRELOAD") + a[keyValuePair.Key] = MergeLDPreload(a[keyValuePair.Key], keyValuePair.Value); + else + a[keyValuePair.Key] = keyValuePair.Value; + } else a.Add(keyValuePair.Key, keyValuePair.Value); } } + private string MergeLDPreload(string a, string b) + { + a ??= ""; + b ??= ""; + return (a.Trim(':') + ":" + b.Trim(':')).Trim(':'); + } + + public void AddEnvironmentVar(string key, string value) + { + extraEnvironmentVars.Add(key, value); + } + + public void AddEnvironmentVars(IDictionary env) + { + MergeDictionaries(extraEnvironmentVars, env); + } + private Process RunInPrefix(ProcessStartInfo psi, string workingDirectory, IDictionary environment, bool redirectOutput, bool writeLog, bool wineD3D) { psi.RedirectStandardOutput = redirectOutput; @@ -156,48 +189,38 @@ private Process RunInPrefix(ProcessStartInfo psi, string workingDirectory, IDict psi.UseShellExecute = false; psi.WorkingDirectory = workingDirectory; - var wineEnviromentVariables = new Dictionary(); - wineEnviromentVariables.Add("WINEPREFIX", Settings.Prefix.FullName); - wineEnviromentVariables.Add("WINEDLLOVERRIDES", $"msquic=,mscoree=n,b;d3d9,d3d11,d3d10core,dxgi={(wineD3D ? "b" : "n")}"); + wineD3D = !DxvkSettings.Enabled || wineD3D; + + var wineEnvironmentVariables = new Dictionary(); + wineEnvironmentVariables.Add("WINEPREFIX", Settings.Prefix.FullName); + wineEnvironmentVariables.Add("WINEDLLOVERRIDES", $"msquic=,mscoree=n,b;d3d9,d3d11,d3d10core,dxgi={(wineD3D ? "b" : "n,b")}"); if (!string.IsNullOrEmpty(Settings.DebugVars)) { - wineEnviromentVariables.Add("WINEDEBUG", Settings.DebugVars); + wineEnvironmentVariables.Add("WINEDEBUG", Settings.DebugVars); } - wineEnviromentVariables.Add("XL_WINEONLINUX", "true"); - string ldPreload = Environment.GetEnvironmentVariable("LD_PRELOAD") ?? ""; + wineEnvironmentVariables.Add("XL_WINEONLINUX", "true"); - string dxvkHud = hudType switch - { - Dxvk.DxvkHudType.None => "0", - Dxvk.DxvkHudType.Fps => "fps", - Dxvk.DxvkHudType.Full => "full", - _ => throw new ArgumentOutOfRangeException() - }; - - if (this.gamemodeOn == true && !ldPreload.Contains("libgamemodeauto.so.0")) - { - ldPreload = ldPreload.Equals("") ? "libgamemodeauto.so.0" : ldPreload + ":libgamemodeauto.so.0"; - } - - wineEnviromentVariables.Add("DXVK_HUD", dxvkHud); - wineEnviromentVariables.Add("DXVK_ASYNC", dxvkAsyncOn); - wineEnviromentVariables.Add("WINEESYNC", Settings.EsyncOn); - wineEnviromentVariables.Add("WINEFSYNC", Settings.FsyncOn); + if (this.gamemodeOn) + wineEnvironmentVariables.Add("LD_PRELOAD", MergeLDPreload("libgamemodeauto.so.0" , Environment.GetEnvironmentVariable("LD_PRELOAD"))); - wineEnviromentVariables.Add("LD_PRELOAD", ldPreload); + foreach (var dxvkVar in DxvkSettings.Environment) + wineEnvironmentVariables.Add(dxvkVar.Key, dxvkVar.Value); + wineEnvironmentVariables.Add("WINEESYNC", Settings.EsyncOn); + wineEnvironmentVariables.Add("WINEFSYNC", Settings.FsyncOn); - MergeDictionaries(psi.EnvironmentVariables, wineEnviromentVariables); - MergeDictionaries(psi.EnvironmentVariables, environment); + MergeDictionaries(psi.Environment, wineEnvironmentVariables); + MergeDictionaries(psi.Environment, extraEnvironmentVars); // Allow extraEnvironmentVars to override what we set here. + MergeDictionaries(psi.Environment, environment); #if FLATPAK_NOTRIGHTNOW psi.FileName = "flatpak-spawn"; psi.ArgumentList.Insert(0, "--host"); - psi.ArgumentList.Insert(1, Wine64Path); + psi.ArgumentList.Insert(1, Settings.WinePath); - foreach (KeyValuePair envVar in wineEnviromentVariables) + foreach (KeyValuePair envVar in wineEnvironmentVariables) { psi.ArgumentList.Insert(1, $"--env={envVar.Key}={envVar.Value}"); } @@ -257,14 +280,54 @@ public Int32 GetUnixProcessId(Int32 winePid) { var wineDbg = RunInPrefix("winedbg --command \"info procmap\"", redirectOutput: true); var output = wineDbg.StandardOutput.ReadToEnd(); - if (output.Contains("syntax error\n")) - return 0; + if (output.Contains("syntax error\n") || output.Contains("Exception c0000005")) // Proton8 wine changed the error message + { + var processName = GetProcessName(winePid); + return GetUnixProcessIdByName(processName); + } var matchingLines = output.Split('\n', StringSplitOptions.RemoveEmptyEntries).Skip(1).Where( l => int.Parse(l.Substring(1, 8), System.Globalization.NumberStyles.HexNumber) == winePid); var unixPids = matchingLines.Select(l => int.Parse(l.Substring(10, 8), System.Globalization.NumberStyles.HexNumber)).ToArray(); return unixPids.FirstOrDefault(); } + private string GetProcessName(Int32 winePid) + { + var wineDbg = RunInPrefix("winedbg --command \"info proc\"", redirectOutput: true); + var output = wineDbg.StandardOutput.ReadToEnd(); + var matchingLines = output.Split('\n', StringSplitOptions.RemoveEmptyEntries).Skip(1).Where( + l => int.Parse(l.Substring(1, 8), System.Globalization.NumberStyles.HexNumber) == winePid); + var processNames = matchingLines.Select(l => l.Substring(20).Trim('\'')).ToArray(); + return processNames.FirstOrDefault(); + } + + private Int32 GetUnixProcessIdByName(string executableName) + { + int closest = 0; + int early = 0; + var currentProcess = Process.GetCurrentProcess(); // Gets XIVLauncher.Core's process + bool nonunique = false; + foreach (var process in Process.GetProcessesByName(executableName)) + { + if (process.Id < currentProcess.Id) + { + early = process.Id; + continue; // Process was launched before XIVLauncher.Core + } + // Assume that the closest PID to XIVLauncher.Core's is the correct one. But log an error if more than one is found. + if ((closest - currentProcess.Id) > (process.Id - currentProcess.Id) || closest == 0) + { + if (closest != 0) nonunique = true; + closest = process.Id; + } + if (nonunique) Log.Error($"More than one {executableName} found! Selecting the most likely match with process id {closest}."); + } + // Deal with rare edge-case where pid rollover causes the ffxiv pid to be lower than XLCore's. + if (closest == 0 && early != 0) closest = early; + if (closest != 0) Log.Verbose($"Process for {executableName} found using fallback method: {closest}. XLCore pid: {currentProcess.Id}"); + return closest; + } + public string UnixToWinePath(string unixPath) { var launchArguments = new string[] { "winepath", "--windows", unixPath }; @@ -282,7 +345,7 @@ public void AddRegistryKey(string key, string value, string data) public void Kill() { - var psi = new ProcessStartInfo(WineServerPath) + var psi = new ProcessStartInfo(Settings.WineServerPath) { Arguments = "-k" }; diff --git a/src/XIVLauncher.Common.Unix/Compatibility/Dxvk.cs b/src/XIVLauncher.Common.Unix/Compatibility/Dxvk.cs deleted file mode 100644 index 62e3a303..00000000 --- a/src/XIVLauncher.Common.Unix/Compatibility/Dxvk.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.IO; -using System.Net.Http; -using System.Threading.Tasks; -using Serilog; -using XIVLauncher.Common.Util; - -namespace XIVLauncher.Common.Unix.Compatibility; - -public static class Dxvk -{ - private const string DXVK_DOWNLOAD = "https://github.com/Sporif/dxvk-async/releases/download/1.10.1/dxvk-async-1.10.1.tar.gz"; - private const string DXVK_NAME = "dxvk-async-1.10.1"; - - public static async Task InstallDxvk(DirectoryInfo prefix, DirectoryInfo installDirectory) - { - var dxvkPath = Path.Combine(installDirectory.FullName, DXVK_NAME, "x64"); - - if (!Directory.Exists(dxvkPath)) - { - Log.Information("DXVK does not exist, downloading"); - await DownloadDxvk(installDirectory).ConfigureAwait(false); - } - - var system32 = Path.Combine(prefix.FullName, "drive_c", "windows", "system32"); - var files = Directory.GetFiles(dxvkPath); - - foreach (string fileName in files) - { - File.Copy(fileName, Path.Combine(system32, Path.GetFileName(fileName)), true); - } - } - - private static async Task DownloadDxvk(DirectoryInfo installDirectory) - { - using var client = new HttpClient(); - var tempPath = PlatformHelpers.GetTempFileName(); - - File.WriteAllBytes(tempPath, await client.GetByteArrayAsync(DXVK_DOWNLOAD)); - PlatformHelpers.Untar(tempPath, installDirectory.FullName); - - File.Delete(tempPath); - } - - public enum DxvkHudType - { - [SettingsDescription("None", "Show nothing")] - None, - - [SettingsDescription("FPS", "Only show FPS")] - Fps, - - [SettingsDescription("Full", "Show everything")] - Full, - } -} \ No newline at end of file diff --git a/src/XIVLauncher.Common.Unix/Compatibility/DxvkSettings.cs b/src/XIVLauncher.Common.Unix/Compatibility/DxvkSettings.cs new file mode 100644 index 00000000..57a4b23e --- /dev/null +++ b/src/XIVLauncher.Common.Unix/Compatibility/DxvkSettings.cs @@ -0,0 +1,100 @@ +using System.IO; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Linq; +using Serilog; +using XIVLauncher.Common.Unix; + +namespace XIVLauncher.Common.Unix.Compatibility; + +public class DxvkSettings +{ + public bool Enabled { get; } + + public string FolderName { get; } + + public string DownloadUrl { get; } + + public Dictionary Environment { get; } + + private static bool? mangoHudFound = null; + + public DxvkSettings(string folder, string url, string storageFolder, bool async, int maxFrameRate, bool dxvkHudEnabled, string dxvkHudString, bool mangoHudEnabled = false, bool mangoHudCustomIsFile = false, string customMangoHud = "", bool enabled = true) + { + FolderName = folder; + DownloadUrl = url; + Enabled = enabled; + + Environment = new Dictionary + { + { "DXVK_LOG_PATH", Path.Combine(storageFolder, "logs") }, + }; + + if (maxFrameRate != 0) + Environment.Add("DXVK_FRAME_RATE", (maxFrameRate).ToString()); + + if (async) + Environment.Add("DXVK_ASYNC", "1"); + + var dxvkCachePath = new DirectoryInfo(Path.Combine(storageFolder, "compatibilitytool", "dxvk", "cache")); + if (!dxvkCachePath.Exists) dxvkCachePath.Create(); + Environment.Add("DXVK_STATE_CACHE_PATH", Path.Combine(dxvkCachePath.FullName, folder)); + + if (dxvkHudEnabled) + Environment.Add("DXVK_HUD", DxvkHudStringIsValid(dxvkHudString) ? dxvkHudString : "1"); + + if (mangoHudEnabled && MangoHudIsInstalled()) + { + Environment.Add("MANGOHUD", "1"); + if (mangoHudCustomIsFile) + { + if (File.Exists(customMangoHud)) + Environment.Add("MANGOHUD_CONFIGFILE", customMangoHud); + else + Environment.Add("MANGOHUD_CONFIG", ""); + } + else + { + Environment.Add("MANGOHUD_CONFIG", customMangoHud); + } + } + } + + public static bool DxvkHudStringIsValid(string customHud) + { + var ALLOWED_CHARS = "^[0-9a-zA-Z,=.]+$"; + var ALLOWED_WORDS = "^(?:devinfo|fps|frametimes|submissions|drawcalls|pipelines|descriptors|memory|gpuload|version|api|cs|compiler|samplers|scale=(?:[0-9])*(?:.(?:[0-9])+)?)$"; + + if (string.IsNullOrWhiteSpace(customHud)) return false; + if (customHud == "full") return true; + if (customHud == "1") return true; + if (!Regex.IsMatch(customHud, ALLOWED_CHARS)) return false; + + string[] hudvars = customHud.Split(","); + + return hudvars.All(hudvar => Regex.IsMatch(hudvar, ALLOWED_WORDS)); + } + + public static bool MangoHudIsInstalled() + { + if (mangoHudFound is null) + { + mangoHudFound = false; + var options = new EnumerationOptions(); + options.RecurseSubdirectories = true; + options.MaxRecursionDepth = 5; + foreach (var path in LinuxInfo.LibraryPaths) + { + if (!Directory.Exists(path)) + continue; + + if (Directory.GetFiles(path, "libMangoHud.so", options).Length > 0) + { + mangoHudFound = true; + break; + } + } + } + return mangoHudFound ?? false; + } +} \ No newline at end of file diff --git a/src/XIVLauncher.Common.Unix/Compatibility/WineSettings.cs b/src/XIVLauncher.Common.Unix/Compatibility/WineSettings.cs index 9b122f7a..ed8d16df 100644 --- a/src/XIVLauncher.Common.Unix/Compatibility/WineSettings.cs +++ b/src/XIVLauncher.Common.Unix/Compatibility/WineSettings.cs @@ -2,32 +2,38 @@ namespace XIVLauncher.Common.Unix.Compatibility; -public enum WineStartupType +public class WineSettings { - [SettingsDescription("Managed by XIVLauncher", "The game installation and wine setup is managed by XIVLauncher - you can leave it up to us.")] - Managed, + public bool IsManaged { get; private set; } - [SettingsDescription("Custom", "Point XIVLauncher to a custom location containing wine binaries to run the game with.")] - Custom, -} + public string WineServerPath => Path.Combine(BinPath, "wineserver"); -public class WineSettings -{ - public WineStartupType StartupType { get; private set; } - public string CustomBinPath { get; private set; } + public string WinePath => File.Exists(Path.Combine(BinPath, "wine64")) ? Path.Combine(BinPath, "wine64") : Path.Combine(BinPath, "wine"); + + private string BinPath; + + public string FolderName; + + public string DownloadUrl; public string EsyncOn { get; private set; } + public string FsyncOn { get; private set; } public string DebugVars { get; private set; } + public FileInfo LogFile { get; private set; } public DirectoryInfo Prefix { get; private set; } - public WineSettings(WineStartupType? startupType, string customBinPath, string debugVars, FileInfo logFile, DirectoryInfo prefix, bool? esyncOn, bool? fsyncOn) + public WineSettings(bool isManaged, string customBinPath, string managedFolder, string managedUrl, DirectoryInfo storageFolder, string debugVars, FileInfo logFile, DirectoryInfo prefix, bool? esyncOn, bool? fsyncOn) { - this.StartupType = startupType ?? WineStartupType.Custom; - this.CustomBinPath = customBinPath; + // storageFolder is the path to .xlcore folder. managedFolder is the foldername inside the tarball that will be downloaded from managedUrl. + IsManaged = isManaged; + FolderName = managedFolder; + DownloadUrl = managedUrl; + BinPath = (isManaged) ? Path.Combine(storageFolder.FullName, "compatibilitytool", "wine", managedFolder, "bin") : customBinPath; + this.EsyncOn = (esyncOn ?? false) ? "1" : "0"; this.FsyncOn = (fsyncOn ?? false) ? "1" : "0"; this.DebugVars = debugVars; diff --git a/src/XIVLauncher.Common.Unix/LinuxContainer.cs b/src/XIVLauncher.Common.Unix/LinuxContainer.cs new file mode 100644 index 00000000..917bfb5a --- /dev/null +++ b/src/XIVLauncher.Common.Unix/LinuxContainer.cs @@ -0,0 +1,10 @@ +namespace XIVLauncher.Common.Unix; + +public enum LinuxContainer +{ + none, + + flatpak, + + snap, +} \ No newline at end of file diff --git a/src/XIVLauncher.Common.Unix/LinuxDistro.cs b/src/XIVLauncher.Common.Unix/LinuxDistro.cs new file mode 100644 index 00000000..534fce89 --- /dev/null +++ b/src/XIVLauncher.Common.Unix/LinuxDistro.cs @@ -0,0 +1,12 @@ +namespace XIVLauncher.Common.Unix; + +public enum LinuxDistro +{ + ubuntu, + + fedora, + + arch, + + none, +} diff --git a/src/XIVLauncher.Common.Unix/LinuxInfo.cs b/src/XIVLauncher.Common.Unix/LinuxInfo.cs new file mode 100644 index 00000000..181efdf5 --- /dev/null +++ b/src/XIVLauncher.Common.Unix/LinuxInfo.cs @@ -0,0 +1,169 @@ +using System; +using System.Runtime.InteropServices; +using System.Collections.Generic; +using System.IO; +using System.Diagnostics; + +namespace XIVLauncher.Common.Unix; + +public static class LinuxInfo +{ + public static LinuxDistro Package { get; private set; } + + public static LinuxContainer Container { get; private set; } + + public static List LibraryPaths { get; private set; } + + public static string Name { get; private set; } + + public static bool IsLinux { get; private set; } + + static LinuxInfo() + { + LibraryPaths = new List(); + var os = System.Environment.OSVersion; + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + Package = LinuxDistro.none; + Container = LinuxContainer.none; + Name = os.VersionString; + IsLinux = false; + return; + } + + IsLinux = true; + Package = LinuxDistro.none; + Container = LinuxContainer.none; + try + { + if (!File.Exists("/etc/os-release")) + { + Package = LinuxDistro.ubuntu; + Container = LinuxContainer.none; + Name = "Unknown distribution"; + addLibraryPaths(); + return; + } + var osRelease = File.ReadAllLines("/etc/os-release"); + var osInfo = new Dictionary(); + foreach (var line in osRelease) + { + var keyValue = line.Split('=', 2); + if (keyValue.Length == 1) + osInfo.Add(keyValue[0], ""); + else + osInfo.Add(keyValue[0], keyValue[1]); + } + + var name = (osInfo.ContainsKey("NAME") ? osInfo["NAME"] : "").Trim('"'); + var pretty = (osInfo.ContainsKey("PRETTY_NAME") ? osInfo["PRETTY_NAME"] : "").Trim('"'); + Name = pretty == "" ? (name == "" ? "Unknown distribution" : name) : pretty; + + // Check for flatpak or snap + if (osInfo.ContainsKey("ID")) + { + if (osInfo["ID"] == "org.freedesktop.platform") + { + Container = LinuxContainer.flatpak; + Package = LinuxDistro.ubuntu; + } + else if (osInfo.ContainsKey("HOME_URL")) + { + if (osInfo["ID"] == "ubuntu-core" && osInfo["HOME_URL"] == "https://snapcraft.io") + { + Container = LinuxContainer.snap; + Package = LinuxDistro.ubuntu; + } + } + } + + // Check distro package if not a container + if (Container == LinuxContainer.none) + { + foreach (var kvp in osInfo) + { + if (kvp.Value.ToLower().Contains("fedora")) + { + Package = LinuxDistro.fedora; + break; + } + else if (kvp.Value.ToLower().Contains("tumbleweed")) + { + Package = LinuxDistro.fedora; + break; + } + else if (kvp.Value.ToLower().Contains("arch")) + { + Package = LinuxDistro.arch; + break; + } + else if (kvp.Value.ToLower().Contains("ubuntu") || kvp.Value.ToLower().Contains("debian")) + { + Package = LinuxDistro.ubuntu; + break; + } + } + if (Package == LinuxDistro.none) + { + Package = LinuxDistro.ubuntu; + } + } + addLibraryPaths(); + } + catch + { + // If there's any kind of error opening the file or even finding it, just go with default. + Package = LinuxDistro.ubuntu; + Name = "Unknown distribution"; + addLibraryPaths(); + } + } + + private static void addLibraryPaths() + { + switch (Container) + { + case LinuxContainer.flatpak: + LibraryPaths.Add(Path.Combine("/", "app", "lib")); + LibraryPaths.Add(Path.Combine("/", "usr", "lib", "x84_64-linux-gnu")); + LibraryPaths.Add(Path.Combine("/", "usr", "lib", "extensions")); + break; + + case LinuxContainer.snap: + LibraryPaths.Add(Path.Combine("/", "usr", "lib", "x86_64-linux-gnu")); + LibraryPaths.Add(Path.Combine("/", "usr", "lib")); + // nvidia host path, needed for dlss on steam snap. These paths look on the host distro. + LibraryPaths.Add(Path.Combine("/", "var", "lib", "snapd", "hostfs", "usr", "lib", "x86_64-linux-gnu", "nvidia")); + LibraryPaths.Add(Path.Combine("/", "var", "lib", "snapd", "hostfs", "usr", "lib64", "nvidia")); + LibraryPaths.Add(Path.Combine("/", "var", "lib", "snapd", "hostfs", "usr", "lib", "nvidia")); + break; + + case LinuxContainer.none: + if (Package == LinuxDistro.none && IsLinux) + Package = LinuxDistro.ubuntu; + switch (Package) + { + case LinuxDistro.arch: + LibraryPaths.Add(Path.Combine("/", "usr", "lib")); + break; + + case LinuxDistro.fedora: + LibraryPaths.Add(Path.Combine("/", "usr", "lib64")); + break; + + case LinuxDistro.ubuntu: + LibraryPaths.Add(Path.Combine("/", "usr", "lib", "x86_64-linux-gnu")); + LibraryPaths.Add(Path.Combine("/", "usr", "lib64")); + LibraryPaths.Add(Path.Combine("/", "usr", "lib")); + LibraryPaths.Add(Path.Combine("/", "lib64")); + LibraryPaths.Add(Path.Combine("/", "lib")); + break; + + case LinuxDistro.none: + break; + } + break; + } + + } +} \ No newline at end of file diff --git a/src/XIVLauncher.Common.Unix/UnixDalamudRunner.cs b/src/XIVLauncher.Common.Unix/UnixDalamudRunner.cs index ef1b8545..b2fde02c 100644 --- a/src/XIVLauncher.Common.Unix/UnixDalamudRunner.cs +++ b/src/XIVLauncher.Common.Unix/UnixDalamudRunner.cs @@ -74,12 +74,28 @@ public UnixDalamudRunner(CompatibilityTools compatibility, DirectoryInfo dotnetR launchArguments.Add(gameArgs); var dalamudProcess = compatibility.RunInPrefix(string.Join(" ", launchArguments), environment: environment, redirectOutput: true, writeLog: true); - var output = dalamudProcess.StandardOutput.ReadLine(); - if (output == null) - throw new DalamudRunnerException("An internal Dalamud error has occured"); + DalamudConsoleOutput dalamudConsoleOutput = null; + int invalidJsonCount = 0; - Console.WriteLine(output); + // Keep checking for valid json output, but only 5 times. If it's still erroring out at that point, give up. + while (dalamudConsoleOutput == null && invalidJsonCount < 5) + { + var output = dalamudProcess.StandardOutput.ReadLine(); + if (output == null) + throw new DalamudRunnerException("An internal Dalamud error has occured"); + Console.WriteLine(output); + + try + { + dalamudConsoleOutput = JsonConvert.DeserializeObject(output); + } + catch (Exception ex) + { + Log.Warning(ex, $"Couldn't parse Dalamud output: {output}"); + } + invalidJsonCount++; + } new Thread(() => { @@ -89,17 +105,15 @@ public UnixDalamudRunner(CompatibilityTools compatibility, DirectoryInfo dotnetR if (output != null) Console.WriteLine(output); } - }).Start(); try { - var dalamudConsoleOutput = JsonConvert.DeserializeObject(output); var unixPid = compatibility.GetUnixProcessId(dalamudConsoleOutput.Pid); if (unixPid == 0) { - Log.Error("Could not retrive Unix process ID, this feature currently requires a patched wine version"); + Log.Error("Could not retrieve Unix process ID"); return null; } @@ -107,9 +121,9 @@ public UnixDalamudRunner(CompatibilityTools compatibility, DirectoryInfo dotnetR Log.Verbose($"Got game process handle {gameProcess.Handle} with Unix pid {gameProcess.Id} and Wine pid {dalamudConsoleOutput.Pid}"); return gameProcess; } - catch (JsonReaderException ex) + catch (Exception ex) { - Log.Error(ex, $"Couldn't parse Dalamud output: {output}"); + Log.Error(ex, $"Could not retrieve game Process information"); return null; } } diff --git a/src/XIVLauncher.Core/Components/MainPage/MainPage.cs b/src/XIVLauncher.Core/Components/MainPage/MainPage.cs index 73500b0d..8c3df63a 100644 --- a/src/XIVLauncher.Core/Components/MainPage/MainPage.cs +++ b/src/XIVLauncher.Core/Components/MainPage/MainPage.cs @@ -23,6 +23,7 @@ using XIVLauncher.Common.Windows; using XIVLauncher.Core.Accounts; using XIVLauncher.Core.Support; +using XIVLauncher.Core.UnixCompatibility; namespace XIVLauncher.Core.Components.MainPage; @@ -751,16 +752,16 @@ public async Task StartGameAndAddon(Launcher.LoginResult loginResult, b } else if (Environment.OSVersion.Platform == PlatformID.Unix) { - if (App.Settings.WineStartupType == WineStartupType.Custom) + if (App.Settings.WineType.Value == WineType.Custom) { if (App.Settings.WineBinaryPath == null) throw new InvalidOperationException("Custom wine binary path wasn't set."); else if (!Directory.Exists(App.Settings.WineBinaryPath)) throw new InvalidOperationException("Custom wine binary path is invalid: no such directory.\n" + - "Check path carefully for typos: " + App.Settings.WineBinaryPath); - else if (!File.Exists(Path.Combine(App.Settings.WineBinaryPath, "wine64"))) - throw new InvalidOperationException("Custom wine binary path is invalid: no wine64 found at that location.\n" + - "Check path carefully for typos: " + App.Settings.WineBinaryPath); + "Check path carefully for typos: " + App.Settings.WineBinaryPath); + else if (!File.Exists(Path.Combine(App.Settings.WineBinaryPath, "wine64")) && !File.Exists(Path.Combine(App.Settings.WineBinaryPath, "wine"))) + throw new InvalidOperationException("Custom wine binary path is invalid: no wine or wine64 found at that location.\n" + + "Check path carefully for typos: " + App.Settings.WineBinaryPath); } var signal = new ManualResetEvent(false); @@ -769,10 +770,8 @@ public async Task StartGameAndAddon(Launcher.LoginResult loginResult, b var _ = Task.Run(async () => { var tempPath = App.Storage.GetFolder("temp"); - var winver = (App.Settings.SetWin7 ?? true) ? "win7" : "win10"; await Program.CompatibilityTools.EnsureTool(tempPath).ConfigureAwait(false); - Program.CompatibilityTools.RunInPrefix($"winecfg /v {winver}"); var gameFixApply = new GameFixApply(App.Settings.GamePath, App.Settings.GameConfigPath, Program.CompatibilityTools.Settings.Prefix, tempPath); gameFixApply.UpdateProgress += (text, hasProgress, progress) => diff --git a/src/XIVLauncher.Core/Components/SettingsPage/DictionarySettingsEntry.cs b/src/XIVLauncher.Core/Components/SettingsPage/DictionarySettingsEntry.cs new file mode 100644 index 00000000..04da17a2 --- /dev/null +++ b/src/XIVLauncher.Core/Components/SettingsPage/DictionarySettingsEntry.cs @@ -0,0 +1,86 @@ +using ImGuiNET; +using XIVLauncher.Core.UnixCompatibility; + +namespace XIVLauncher.Core.Components.SettingsPage; + +public class DictionarySettingsEntry : SettingsEntry +{ + public Dictionary> Pairs; + + public string DefaultValue; + + public bool ShowDescription; + + public bool ShowItemDescription; + + public DictionarySettingsEntry(string name, string description, Dictionary> pairs, Func load, Action save, string defaultValue, bool showSelectedDesc = false, bool showItemDesc = true) + : base(name, description, load, save) + { + this.Pairs = pairs; + this.DefaultValue = defaultValue; + this.ShowDescription = showSelectedDesc; + this.ShowItemDescription = showItemDesc; + } + + + public override void Draw() + { + var nativeValue = this.Value; + string idx = (string)(this.InternalValue ?? DefaultValue); + + ImGuiHelpers.TextWrapped(this.Name); + + Dictionary>.KeyCollection keys = Pairs.Keys; + var label = Pairs[idx].ContainsKey("label") ? $"[{Pairs[idx]["label"]}] " : ""; + var name = Pairs[idx].ContainsKey("name") ? Pairs[idx]["name"] : idx; + var desc = ShowDescription && Pairs[idx].ContainsKey("desc") ? $" - {Pairs[idx]["desc"]}" : ""; + var mark = Pairs[idx].ContainsKey("mark") ? $" *{Pairs[idx]["mark"]}*" : ""; + + if (ImGui.BeginCombo($"###{Id.ToString()}", $"{label}{name}{desc}{mark}")) + { + foreach ( string key in keys ) + { + var itemlabel = Pairs[key].ContainsKey("label") ? $"[{Pairs[key]["label"]}] " : ""; + var itemname = Pairs[key].ContainsKey("name") ? Pairs[key]["name"] : key; + var itemdesc = ShowItemDescription && Pairs[key].ContainsKey("desc") ? $" - {Pairs[key]["desc"]}" : ""; + var itemmark = Pairs[key].ContainsKey("mark") ? $" *{Pairs[key]["mark"]}*" : ""; + if (ImGui.Selectable($"{itemlabel}{itemname}{itemdesc}{itemmark}", idx == key)) + { + this.InternalValue = key; + } + } + ImGui.EndCombo(); + } + + ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey); + if (!string.IsNullOrEmpty(this.Description)) ImGuiHelpers.TextWrapped(this.Description); + ImGui.PopStyleColor(); + + if (this.CheckValidity != null) + { + var validityMsg = this.CheckValidity.Invoke(this.Value); + this.IsValid = string.IsNullOrEmpty(validityMsg); + + if (!this.IsValid) + { + ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed); + ImGui.Text(validityMsg); + ImGui.PopStyleColor(); + } + } + else + { + this.IsValid = true; + } + + var warningMessage = this.CheckWarning?.Invoke(this.Value); + + if (warningMessage != null) + { + ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed); + ImGui.Text(warningMessage); + ImGui.PopStyleColor(); + } + + } +} \ No newline at end of file diff --git a/src/XIVLauncher.Core/Components/SettingsPage/SettingsEntry{T}.cs b/src/XIVLauncher.Core/Components/SettingsPage/SettingsEntry{T}.cs index 20ce9b33..c1da293d 100644 --- a/src/XIVLauncher.Core/Components/SettingsPage/SettingsEntry{T}.cs +++ b/src/XIVLauncher.Core/Components/SettingsPage/SettingsEntry{T}.cs @@ -93,7 +93,7 @@ public override void Draw() } ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey); - ImGuiHelpers.TextWrapped(this.Description); + if (!string.IsNullOrWhiteSpace(this.Description)) ImGuiHelpers.TextWrapped(this.Description); ImGui.PopStyleColor(); if (this.CheckValidity != null) diff --git a/src/XIVLauncher.Core/Components/SettingsPage/SettingsPage.cs b/src/XIVLauncher.Core/Components/SettingsPage/SettingsPage.cs index 6c8ed064..464909a3 100644 --- a/src/XIVLauncher.Core/Components/SettingsPage/SettingsPage.cs +++ b/src/XIVLauncher.Core/Components/SettingsPage/SettingsPage.cs @@ -13,6 +13,7 @@ public class SettingsPage : Page new SettingsTabGame(), new SettingsTabPatching(), new SettingsTabWine(), + new SettingsTabDxvk(), new SettingsTabDalamud(), new SettingsTabAbout(), new SettingsTabDebug(), diff --git a/src/XIVLauncher.Core/Components/SettingsPage/SettingsTab.cs b/src/XIVLauncher.Core/Components/SettingsPage/SettingsTab.cs index 5ec75444..cda95de2 100644 --- a/src/XIVLauncher.Core/Components/SettingsPage/SettingsTab.cs +++ b/src/XIVLauncher.Core/Components/SettingsPage/SettingsTab.cs @@ -17,9 +17,10 @@ public override void Draw() foreach (SettingsEntry settingsEntry in Entries) { if (settingsEntry.IsVisible) + { settingsEntry.Draw(); - - ImGui.Dummy(new Vector2(10) * ImGuiHelpers.GlobalScale); + ImGui.Dummy(new Vector2(10) * ImGuiHelpers.GlobalScale); + } } base.Draw(); diff --git a/src/XIVLauncher.Core/Components/SettingsPage/Tabs/SettingsTabDebug.cs b/src/XIVLauncher.Core/Components/SettingsPage/Tabs/SettingsTabDebug.cs index 12d15625..5104d355 100644 --- a/src/XIVLauncher.Core/Components/SettingsPage/Tabs/SettingsTabDebug.cs +++ b/src/XIVLauncher.Core/Components/SettingsPage/Tabs/SettingsTabDebug.cs @@ -1,6 +1,9 @@ using System.Collections; using ImGuiNET; +using XIVLauncher.Core; +using XIVLauncher.Common; +using XIVLauncher.Common.Unix; namespace XIVLauncher.Core.Components.SettingsPage.Tabs; @@ -13,18 +16,24 @@ public override void Draw() { ImGui.TextUnformatted("Generic Information"); ImGui.Separator(); - ImGui.TextUnformatted($"Operating System: {Environment.OSVersion}"); - ImGui.TextUnformatted($"Runtime Version: {Environment.Version}"); - + if (LinuxInfo.IsLinux) + ImGui.TextUnformatted($"Operating System: {LinuxInfo.Name} - {Environment.OSVersion}"); + else + ImGui.TextUnformatted($"Operating System: {Environment.OSVersion}"); + ImGui.TextUnformatted($"Runtime Version: {Environment.Version}"); + if (Program.IsSteamDeckHardware) ImGui.Text("Steam Deck Hardware Detected"); if (Program.IsSteamDeckGamingMode) ImGui.Text("Steam Deck Gaming Mode Detected"); -#if FLATPAK - ImGui.Text("Running as a Flatpak"); -#endif + if (LinuxInfo.Container == LinuxContainer.flatpak) + ImGui.Text("Running in a Flatpak container"); + + if (LinuxInfo.Container == LinuxContainer.snap) + ImGui.Text("Running in a Snap container"); + ImGui.Spacing(); diff --git a/src/XIVLauncher.Core/Components/SettingsPage/Tabs/SettingsTabDxvk.cs b/src/XIVLauncher.Core/Components/SettingsPage/Tabs/SettingsTabDxvk.cs new file mode 100644 index 00000000..a8374345 --- /dev/null +++ b/src/XIVLauncher.Core/Components/SettingsPage/Tabs/SettingsTabDxvk.cs @@ -0,0 +1,99 @@ +using System.Runtime.InteropServices; +using ImGuiNET; +using XIVLauncher.Common.Unix.Compatibility; +using XIVLauncher.Core.UnixCompatibility; + +namespace XIVLauncher.Core.Components.SettingsPage.Tabs; + +public class SettingsTabDxvk : SettingsTab +{ + private DictionarySettingsEntry dxvkVersionSetting; + private SettingsEntry dxvkHudSetting; + private SettingsEntry mangoHudSetting; + + private string dxvkPath = Path.Combine(Program.storage.Root.FullName, "compatibilitytool", "dxvk"); + + public SettingsTabDxvk() + { + Entries = new SettingsEntry[] + { + dxvkVersionSetting = new DictionarySettingsEntry("DXVK Version", $"Choose which version of DXVK to use. Put your custom DXVK in {dxvkPath}\nEntries marked with *Download* will be downloaded when you log in.", Dxvk.Versions, () => Program.Config.DxvkVersion ?? "dxvk-async-1.10.3", s => Program.Config.DxvkVersion = s, Dxvk.GetDefaultVersion()) + { + CheckWarning = s => + { + if (s is null) return null; + if (s.StartsWith("dxvk-2") || s.StartsWith("dxvk-async-2") || s.StartsWith("dxvk-gplasync-v2")) + return "May not work with older graphics cards. AMD users may need to use env variable RADV_PERFTEST=gpl"; + return null; + }, + }, + new SettingsEntry("Enable DXVK ASYNC", "Enable DXVK ASYNC patch. May not be available on DXVK >= 2.0", () => Program.Config.DxvkAsyncEnabled ?? true, b => Program.Config.DxvkAsyncEnabled = b) + { + CheckVisibility = () => dxvkVersionSetting.Value != "DISABLED", + }, + + dxvkHudSetting = new SettingsEntry("DXVK Overlay", "DXVK Hud is included with DXVK. MangoHud must be installed separately.\nFlatpak users need the flatpak version of MangoHud.", () => Program.Config.DxvkHud ?? DxvkHud.None, x => Program.Config.DxvkHud = x) + { + CheckVisibility = () => dxvkVersionSetting.Value != "DISABLED", + }, + + new SettingsEntry("DXVK Hud Custom String", "Set a custom string for the built in DXVK Hud. Warning: If it's invalid, the game may hang.", () => Program.Config.DxvkHudCustom ?? Dxvk.DXVK_HUD, s => Program.Config.DxvkHudCustom = s) + { + CheckVisibility = () => dxvkHudSetting.Value == DxvkHud.Custom && dxvkVersionSetting.Value != "DISABLED", + CheckWarning = s => + { + if(!DxvkSettings.DxvkHudStringIsValid(s)) + return "That's not a valid hud string"; + return null; + }, + }, + mangoHudSetting = new SettingsEntry("MangoHud Overlay", "MangoHud is installed. It is recommended to set Dxvk Overlay to None if using MangoHud.", () => Program.Config.MangoHud ?? MangoHud.None, x => Program.Config.MangoHud = x) + { + CheckVisibility = () => dxvkVersionSetting.Value != "DISABLED" && Dxvk.MangoHudInstalled, + CheckWarning = x => + { + if (dxvkHudSetting.Value != DxvkHud.None && x != MangoHud.None) + return "Warning! You can run Dxvk Hud and MangoHud at the same time, but you probably shouldn't.\nSet one of them to None."; + return null; + } + }, + new SettingsEntry("MangoHud Custom String", "Set a custom string for MangoHud config.", () => Program.Config.MangoHudCustomString ?? Dxvk.MANGOHUD_CONFIG, s => Program.Config.MangoHudCustomString = s) + { + CheckVisibility = () => mangoHudSetting.Value == MangoHud.CustomString && dxvkVersionSetting.Value != "DISABLED" && Dxvk.MangoHudInstalled, + CheckWarning = s => + { + if (s.Contains(' ')) + return "No spaces allowed in MangoHud config"; + return null; + } + }, + new SettingsEntry("MangoHud Custom Path", "Set a custom path for MangoHud config file.", () => Program.Config.MangoHudCustomFile ?? Dxvk.MANGOHUD_CONFIGFILE, s => Program.Config.MangoHudCustomFile = s) + { + CheckVisibility = () => mangoHudSetting.Value == MangoHud.CustomFile && dxvkVersionSetting.Value != "DISABLED" && Dxvk.MangoHudInstalled, + CheckWarning = s => + { + if(!File.Exists(s)) + return "That's not a valid file."; + return null; + }, + }, + new NumericSettingsEntry("Frame Rate Limit", "Set a frame rate limit, and DXVK will try not exceed it. Use 0 for unlimited.", () => Program.Config.DxvkFrameRateLimit ?? 0, i => Program.Config.DxvkFrameRateLimit = i, 0, 1000) + { + CheckVisibility = () => dxvkVersionSetting.Value != "DISABLED", + }, + }; + } + + public override SettingsEntry[] Entries { get; } + + public override bool IsUnixExclusive => true; + + public override string Title => "DXVK"; + + public override void Save() + { + base.Save(); + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + Program.CreateCompatToolsInstance(); + } +} diff --git a/src/XIVLauncher.Core/Components/SettingsPage/Tabs/SettingsTabTroubleshooting.cs b/src/XIVLauncher.Core/Components/SettingsPage/Tabs/SettingsTabTroubleshooting.cs index 2bb8141f..f51f9a0c 100644 --- a/src/XIVLauncher.Core/Components/SettingsPage/Tabs/SettingsTabTroubleshooting.cs +++ b/src/XIVLauncher.Core/Components/SettingsPage/Tabs/SettingsTabTroubleshooting.cs @@ -38,16 +38,22 @@ public override void Draw() Program.ClearPrefix(); } - ImGui.Text("\nClear the managed Wine install and DXVK"); + ImGui.Text("\nClear the managed Wine and DXVK installs. Custom versions won't be touched."); if (ImGui.Button("Clear Wine & DXVK")) { Program.ClearTools(true); } - ImGui.Text("\nClear all the files and folders related to Dalamud. Your settings will not be touched,\nbut all your plugins will be uninstalled, including custom repos."); + ImGui.Text("\nClear all the files and folders related to Dalamud. This will not uninstall your plugins or their configurations."); if (ImGui.Button("Clear Dalamud")) { - Program.ClearPlugins(true); + Program.ClearDalamud(true); + } + + ImGui.Text("\nClear the installedPlugins folder. This will uninstall your plugins, but will not remove their configurations."); + if (ImGui.Button("Clear Plugins")) + { + Program.ClearPlugins(); } ImGui.Text("\nClear all the log files."); @@ -68,6 +74,12 @@ public override void Draw() Program.ClearAll(true); } + ImGui.Text("\nOpen the .xlcore folder in your file browser."); + if (ImGui.Button("Open .xlcore")) + { + PlatformHelpers.OpenBrowser(Program.storage.Root.FullName); + } + ImGui.Text("\nGenerate a troubleshooting pack to upload to the official Discord channel"); if (ImGui.Button("Generate tspack")) { diff --git a/src/XIVLauncher.Core/Components/SettingsPage/Tabs/SettingsTabWine.cs b/src/XIVLauncher.Core/Components/SettingsPage/Tabs/SettingsTabWine.cs index b9a95b1b..05e1e8a5 100644 --- a/src/XIVLauncher.Core/Components/SettingsPage/Tabs/SettingsTabWine.cs +++ b/src/XIVLauncher.Core/Components/SettingsPage/Tabs/SettingsTabWine.cs @@ -5,25 +5,33 @@ using XIVLauncher.Common.Unix.Compatibility; using XIVLauncher.Common.Util; +using XIVLauncher.Core.UnixCompatibility; namespace XIVLauncher.Core.Components.SettingsPage.Tabs; public class SettingsTabWine : SettingsTab { - private SettingsEntry startupTypeSetting; + private SettingsEntry wineTypeSetting; + + private readonly string toolDirectory = Path.Combine(Program.storage.Root.FullName, "compatibilitytool", "wine"); public SettingsTabWine() { Entries = new SettingsEntry[] { - startupTypeSetting = new SettingsEntry("Wine Version", "Choose how XIVLauncher will start and manage your wine installation.", - () => Program.Config.WineStartupType ?? WineStartupType.Managed, x => Program.Config.WineStartupType = x), + wineTypeSetting = new SettingsEntry("Installation Type", "Choose how XIVLauncher will start and manage your game installation.", + () => Program.Config.WineType ?? WineType.Managed, x => Program.Config.WineType = x), + new DictionarySettingsEntry("Wine Version", $"Wine versions in {toolDirectory}\nEntries marked with *Download* will be downloaded when you log in.", Wine.Versions, () => Program.Config.WineVersion, s => Program.Config.WineVersion = s, Wine.GetDefaultVersion()) + { + CheckVisibility = () => wineTypeSetting.Value == WineType.Managed + }, + new SettingsEntry("Wine Binary Path", "Set the path XIVLauncher will use to run applications via wine.\nIt should be an absolute path to a folder containing wine64 and wineserver binaries.", () => Program.Config.WineBinaryPath, s => Program.Config.WineBinaryPath = s) { - CheckVisibility = () => startupTypeSetting.Value == WineStartupType.Custom + CheckVisibility = () => wineTypeSetting.Value == WineType.Custom }, new SettingsEntry("Enable Feral's GameMode", "Enable launching with Feral Interactive's GameMode CPU optimizations.", () => Program.Config.GameModeEnabled ?? true, b => Program.Config.GameModeEnabled = b) @@ -31,16 +39,14 @@ public SettingsTabWine() CheckVisibility = () => RuntimeInformation.IsOSPlatform(OSPlatform.Linux), CheckValidity = b => { - var handle = IntPtr.Zero; - if (b == true && !NativeLibrary.TryLoad("libgamemodeauto.so.0", out handle)) + if (b == true && !CoreEnvironmentSettings.IsGameModeInstalled()) return "GameMode was not detected on your system."; - NativeLibrary.Free(handle); return null; } }, - new SettingsEntry("Enable DXVK ASYNC", "Enable DXVK ASYNC patch.", () => Program.Config.DxvkAsyncEnabled ?? true, b => Program.Config.DxvkAsyncEnabled = b), new SettingsEntry("Enable ESync", "Enable eventfd-based synchronization.", () => Program.Config.ESyncEnabled ?? true, b => Program.Config.ESyncEnabled = b), + new SettingsEntry("Enable FSync", "Enable fast user mutex (futex2).", () => Program.Config.FSyncEnabled ?? true, b => Program.Config.FSyncEnabled = b) { CheckVisibility = () => RuntimeInformation.IsOSPlatform(OSPlatform.Linux), @@ -53,9 +59,6 @@ public SettingsTabWine() } }, - new SettingsEntry("Set Windows version to 7", "Default for Wine 8.1+ is Windows 10, but this causes issues with some Dalamud plugins. Windows 7 is recommended for now.", () => Program.Config.SetWin7 ?? true, b => Program.Config.SetWin7 = b), - - new SettingsEntry("DXVK Overlay", "Configure how much of the DXVK overlay is to be shown.", () => Program.Config.DxvkHudType, type => Program.Config.DxvkHudType = type), new SettingsEntry("WINEDEBUG Variables", "Configure debug logging for wine. Useful for troubleshooting.", () => Program.Config.WineDebugVars ?? string.Empty, s => Program.Config.WineDebugVars = s) }; } @@ -70,6 +73,10 @@ public override void Draw() { base.Draw(); + ImGui.Separator(); + + ImGui.Dummy(new Vector2(10) * ImGuiHelpers.GlobalScale); + if (!Program.CompatibilityTools.IsToolDownloaded) { ImGui.BeginDisabled(); @@ -97,6 +104,31 @@ public override void Draw() Program.CompatibilityTools.RunInPrefix("explorer"); } + ImGui.SameLine(); + + if (ImGui.Button("Open Wine explorer (use WineD3D")) + { + Program.CompatibilityTools.RunInPrefix("explorer", wineD3D: true); + + } + + ImGui.Dummy(new Vector2(10) * ImGuiHelpers.GlobalScale); + + + if (ImGui.Button("Set Wine to Windows 7")) + { + Program.CompatibilityTools.RunInPrefix($"winecfg /v win7", redirectOutput: true, writeLog: true); + } + + ImGui.SameLine(); + + if (ImGui.Button("Set Wine to Windows 10")) + { + Program.CompatibilityTools.RunInPrefix($"winecfg /v win10", redirectOutput: true, writeLog: true); + } + + ImGui.Dummy(new Vector2(10) * ImGuiHelpers.GlobalScale); + if (ImGui.Button("Kill all wine processes")) { Program.CompatibilityTools.Kill(); diff --git a/src/XIVLauncher.Core/Configuration/ILauncherConfig.cs b/src/XIVLauncher.Core/Configuration/ILauncherConfig.cs index d24fadf1..9fef0eb1 100644 --- a/src/XIVLauncher.Core/Configuration/ILauncherConfig.cs +++ b/src/XIVLauncher.Core/Configuration/ILauncherConfig.cs @@ -3,6 +3,7 @@ using XIVLauncher.Common.Dalamud; using XIVLauncher.Common.Game.Patch.Acquisition; using XIVLauncher.Common.Unix.Compatibility; +using XIVLauncher.Core.UnixCompatibility; namespace XIVLauncher.Core.Configuration; @@ -60,7 +61,9 @@ public interface ILauncherConfig #region Linux - public WineStartupType? WineStartupType { get; set; } + public WineType? WineType { get; set; } + + public string? WineVersion { get; set; } public string? WineBinaryPath { get; set; } @@ -72,7 +75,19 @@ public interface ILauncherConfig public bool? FSyncEnabled { get; set; } - public Dxvk.DxvkHudType DxvkHudType { get; set; } + public string? DxvkVersion { get; set; } + + public int? DxvkFrameRateLimit { get; set; } + + public DxvkHud? DxvkHud { get; set; } + + public string? DxvkHudCustom { get; set; } + + public MangoHud? MangoHud { get; set; } + + public string? MangoHudCustomString { get; set; } + + public string? MangoHudCustomFile { get; set; } public string? WineDebugVars { get; set; } @@ -82,8 +97,6 @@ public interface ILauncherConfig public bool? FixIM { get; set; } - public bool? SetWin7 { get; set; } - #endregion #region Dalamud diff --git a/src/XIVLauncher.Core/CoreEnvironmentSettings.cs b/src/XIVLauncher.Core/CoreEnvironmentSettings.cs index 51c89987..2fcb70d2 100644 --- a/src/XIVLauncher.Core/CoreEnvironmentSettings.cs +++ b/src/XIVLauncher.Core/CoreEnvironmentSettings.cs @@ -1,15 +1,20 @@ using System.Diagnostics; +using System.Globalization; +using System.Runtime.InteropServices; namespace XIVLauncher.Core; public static class CoreEnvironmentSettings { public static bool? IsDeck => CheckEnvBoolOrNull("XL_DECK"); + public static bool IsSteamDeckVar => CheckEnvBool("SteamDeck"); public static bool? IsDeckGameMode => CheckEnvBoolOrNull("XL_GAMEMODE"); + public static bool IsSteamGamepadUIVar => CheckEnvBool("SteamGamepadUI"); public static bool? IsDeckFirstRun => CheckEnvBoolOrNull("XL_FIRSTRUN"); public static bool IsUpgrade => CheckEnvBool("XL_SHOW_UPGRADE"); public static bool ClearSettings => CheckEnvBool("XL_CLEAR_SETTINGS"); public static bool ClearPrefix => CheckEnvBool("XL_CLEAR_PREFIX"); + public static bool ClearDalamud => CheckEnvBool("XL_CLEAR_DALAMUD"); public static bool ClearPlugins => CheckEnvBool("XL_CLEAR_PLUGINS"); public static bool ClearTools => CheckEnvBool("XL_CLEAR_TOOLS"); public static bool ClearLogs => CheckEnvBool("XL_CLEAR_LOGS"); @@ -18,6 +23,10 @@ public static class CoreEnvironmentSettings public static bool IsSteamCompatTool => CheckEnvBool("XL_SCT"); public static uint SteamAppId => GetAppId(System.Environment.GetEnvironmentVariable("SteamAppId")); public static uint AltAppID => GetAppId(System.Environment.GetEnvironmentVariable("XL_APPID")); + public static string HOME => System.Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + public static string XDG_CONFIG_HOME => string.IsNullOrEmpty(System.Environment.GetEnvironmentVariable("XDG_CONFIG_HOME")) ? Path.Combine(HOME, ".config") : System.Environment.GetEnvironmentVariable("XDG_CONFIG_HOME") ?? ""; + public static string XDG_DATA_HOME => string.IsNullOrEmpty(System.Environment.GetEnvironmentVariable("XDG_DATA_HOME")) ? Path.Combine(HOME, ".local", "share") : System.Environment.GetEnvironmentVariable("XDG_DATA_HOME") ?? ""; + private static bool CheckEnvBool(string key) { @@ -63,4 +72,21 @@ public static string GetCType() var output = proc.StandardOutput.ReadToEnd().Split('\n', StringSplitOptions.RemoveEmptyEntries); return Array.Find(output, s => s.ToUpper().StartsWith("C.")) ?? string.Empty; } + + static private bool? gameModeInstalled = null; + + static public bool IsGameModeInstalled() + { + if (gameModeInstalled is not null) + return gameModeInstalled ?? false; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + var handle = IntPtr.Zero; + gameModeInstalled = NativeLibrary.TryLoad("libgamemodeauto.so.0", out handle); + NativeLibrary.Free(handle); + } + else + gameModeInstalled = false; + return gameModeInstalled ?? false; + } } diff --git a/src/XIVLauncher.Core/Program.cs b/src/XIVLauncher.Core/Program.cs index e8d4c01e..03a9515f 100644 --- a/src/XIVLauncher.Core/Program.cs +++ b/src/XIVLauncher.Core/Program.cs @@ -21,11 +21,13 @@ using XIVLauncher.Common.Unix.Compatibility; using XIVLauncher.Common.Util; using XIVLauncher.Common.Windows; +using XIVLauncher.Common.Unix; using XIVLauncher.Core.Accounts.Secrets; using XIVLauncher.Core.Accounts.Secrets.Providers; using XIVLauncher.Core.Components.LoadingPage; using XIVLauncher.Core.Configuration; using XIVLauncher.Core.Configuration.Parsers; +using XIVLauncher.Core.UnixCompatibility; using XIVLauncher.Core.Style; namespace XIVLauncher.Core; @@ -58,12 +60,13 @@ sealed class Program public static DirectoryInfo DotnetRuntime => storage.GetFolder("runtime"); // TODO: We don't have the steamworks api for this yet. + // SteamDeck=1 on Steam Deck by default. SteamGamepadUI=1 in Big Picture / Gaming Mode. public static bool IsSteamDeckHardware => CoreEnvironmentSettings.IsDeck.HasValue ? CoreEnvironmentSettings.IsDeck.Value : - Directory.Exists("/home/deck") || (CoreEnvironmentSettings.IsDeckGameMode ?? false) || (CoreEnvironmentSettings.IsDeckFirstRun ?? false); + CoreEnvironmentSettings.IsSteamDeckVar || (CoreEnvironmentSettings.IsDeckGameMode ?? false) || (CoreEnvironmentSettings.IsDeckFirstRun ?? false); public static bool IsSteamDeckGamingMode => CoreEnvironmentSettings.IsDeckGameMode.HasValue ? CoreEnvironmentSettings.IsDeckGameMode.Value : - Steam != null && Steam.IsValid && Steam.IsRunningOnSteamDeck(); + Steam != null && Steam.IsValid && Steam.IsRunningOnSteamDeck() && CoreEnvironmentSettings.IsSteamGamepadUIVar; private const string APP_NAME = "xlcore"; @@ -125,15 +128,25 @@ private static void LoadConfig(Storage storage) Config.GlobalScale ??= 1.0f; Config.GameModeEnabled ??= false; - Config.DxvkAsyncEnabled ??= true; Config.ESyncEnabled ??= true; Config.FSyncEnabled ??= false; - Config.SetWin7 ??= true; - Config.WineStartupType ??= WineStartupType.Managed; + Config.WineType ??= WineType.Managed; + if (!Wine.Versions.ContainsKey(Config.WineVersion ?? "")) + Config.WineVersion = "wine-xiv-staging-fsync-git-8.5.r4.g4211bac7"; Config.WineBinaryPath ??= "/usr/bin"; Config.WineDebugVars ??= "-all"; + if (!Dxvk.Versions.ContainsKey(Config.DxvkVersion ?? "")) + Config.DxvkVersion = "dxvk-async-1.10.3"; + Config.DxvkAsyncEnabled ??= true; + Config.DxvkFrameRateLimit ??= 0; + Config.DxvkHud ??= DxvkHud.None; + Config.DxvkHudCustom ??= Dxvk.DXVK_HUD; + Config.MangoHud ??= MangoHud.None; + Config.MangoHudCustomString ??= Dxvk.MANGOHUD_CONFIG; + Config.MangoHudCustomFile ??= Dxvk.MANGOHUD_CONFIGFILE; + Config.FixLDP ??= false; Config.FixIM ??= false; Config.FixLocale ??= false; @@ -177,6 +190,8 @@ private static void Main(string[] args) { mainArgs = args; storage = new Storage(APP_NAME); + Wine.Initialize(); + Dxvk.Initialize(); if (CoreEnvironmentSettings.ClearAll) { @@ -186,6 +201,7 @@ private static void Main(string[] args) { if (CoreEnvironmentSettings.ClearSettings) ClearSettings(); if (CoreEnvironmentSettings.ClearPrefix) ClearPrefix(); + if (CoreEnvironmentSettings.ClearDalamud) ClearDalamud(); if (CoreEnvironmentSettings.ClearPlugins) ClearPlugins(); if (CoreEnvironmentSettings.ClearTools) ClearTools(); if (CoreEnvironmentSettings.ClearLogs) ClearLogs(); @@ -294,15 +310,13 @@ private static void Main(string[] args) var needUpdate = false; -#if FLATPAK - if (Config.DoVersionCheck ?? false) + if (LinuxInfo.Container == LinuxContainer.flatpak && (Config.DoVersionCheck ?? false)) { var versionCheckResult = UpdateCheck.CheckForUpdate().GetAwaiter().GetResult(); if (versionCheckResult.Success) needUpdate = versionCheckResult.NeedUpdate; } -#endif needUpdate = CoreEnvironmentSettings.IsUpgrade ? true : needUpdate; @@ -357,23 +371,21 @@ private static void Main(string[] args) gd.SwapBuffers(gd.MainSwapchain); } - HttpClient.Dispose(); // Clean up Veldrid resources gd.WaitForIdle(); bindings.Dispose(); cl.Dispose(); gd.Dispose(); + + HttpClient.Dispose(); } public static void CreateCompatToolsInstance() { - var wineLogFile = new FileInfo(Path.Combine(storage.GetFolder("logs").FullName, "wine.log")); - var winePrefix = storage.GetFolder("wineprefix"); - var wineSettings = new WineSettings(Config.WineStartupType, Config.WineBinaryPath, Config.WineDebugVars, wineLogFile, winePrefix, Config.ESyncEnabled, Config.FSyncEnabled); + var dxvkSettings = new DxvkSettings(Dxvk.FolderName, Dxvk.DownloadUrl, storage.Root.FullName, Dxvk.AsyncEnabled, Dxvk.FrameRateLimit, Dxvk.DxvkHudEnabled, Dxvk.DxvkHudString, Dxvk.MangoHudEnabled, Dxvk.MangoHudCustomIsFile, Dxvk.MangoHudString, Dxvk.Enabled); + var wineSettings = new WineSettings(Wine.IsManagedWine, Wine.CustomWinePath, Wine.FolderName, Wine.DownloadUrl, storage.Root, Wine.DebugVars, Wine.LogFile, Wine.Prefix, Wine.ESyncEnabled, Wine.FSyncEnabled); var toolsFolder = storage.GetFolder("compatibilitytool"); - Directory.CreateDirectory(Path.Combine(toolsFolder.FullName, "dxvk")); - Directory.CreateDirectory(Path.Combine(toolsFolder.FullName, "beta")); - CompatibilityTools = new CompatibilityTools(wineSettings, Config.DxvkHudType, Config.GameModeEnabled, Config.DxvkAsyncEnabled, toolsFolder); + CompatibilityTools = new CompatibilityTools(wineSettings, dxvkSettings, Config.GameModeEnabled, toolsFolder); } public static void ShowWindow() @@ -435,17 +447,14 @@ public static void ClearPrefix() storage.GetFolder("wineprefix"); } - public static void ClearPlugins(bool tsbutton = false) + public static void ClearDalamud(bool tsbutton = false) { storage.GetFolder("dalamud").Delete(true); storage.GetFolder("dalamudAssets").Delete(true); - storage.GetFolder("installedPlugins").Delete(true); storage.GetFolder("runtime").Delete(true); if (storage.GetFile("dalamudUI.ini").Exists) storage.GetFile("dalamudUI.ini").Delete(); - if (storage.GetFile("dalamudConfig.json").Exists) storage.GetFile("dalamudConfig.json").Delete(); storage.GetFolder("dalamud"); storage.GetFolder("dalamudAssets"); - storage.GetFolder("installedPlugins"); storage.GetFolder("runtime"); if (tsbutton) { @@ -455,11 +464,31 @@ public static void ClearPlugins(bool tsbutton = false) } } + public static void ClearPlugins() + { + storage.GetFolder("installedPlugins").Delete(true); + storage.GetFolder("installedPlugins"); + if (storage.GetFile("dalamudConfig.json").Exists) storage.GetFile("dalamudConfig.json").Delete(); + } + public static void ClearTools(bool tsbutton = false) { - storage.GetFolder("compatibilitytool").Delete(true); - storage.GetFolder("compatibilitytool/beta"); - storage.GetFolder("compatibilitytool/dxvk"); + foreach (var winetool in Wine.Versions) + { + if (winetool.Value.ContainsKey("url")) + if (!string.IsNullOrEmpty(winetool.Value["url"])) + storage.GetFolder($"compatibilitytool/wine/{winetool.Key}").Delete(true); + } + foreach (var dxvktool in Dxvk.Versions) + { + if (dxvktool.Value.ContainsKey("url")) + if (!string.IsNullOrEmpty(dxvktool.Value["url"])) + storage.GetFolder($"compatibilitytool/dxvk/{dxvktool.Key}").Delete(true); + } + // Re-initialize Versions so they get *Download* marks back. + Wine.Initialize(); + Dxvk.Initialize(); + if (tsbutton) CreateCompatToolsInstance(); } @@ -478,7 +507,8 @@ public static void ClearAll(bool tsbutton = false) { ClearSettings(tsbutton); ClearPrefix(); - ClearPlugins(tsbutton); + ClearDalamud(tsbutton); + ClearPlugins(); ClearTools(tsbutton); ClearLogs(true); } diff --git a/src/XIVLauncher.Core/UnixCompatibility/Dxvk.cs b/src/XIVLauncher.Core/UnixCompatibility/Dxvk.cs new file mode 100644 index 00000000..8608b63d --- /dev/null +++ b/src/XIVLauncher.Core/UnixCompatibility/Dxvk.cs @@ -0,0 +1,174 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using Serilog; +using XIVLauncher.Common; +using XIVLauncher.Common.Unix.Compatibility; + +namespace XIVLauncher.Core.UnixCompatibility; + +public static class Dxvk +{ + public static bool Enabled => Program.Config.DxvkVersion != "DISABLED"; + + public static string FolderName => Program.Config.DxvkVersion ?? GetDefaultVersion(); + + public static string DownloadUrl => GetDownloadUrl(Program.Config.DxvkVersion); + + public static int FrameRateLimit => Program.Config.DxvkFrameRateLimit ?? 0; + + public static bool AsyncEnabled => Program.Config.DxvkAsyncEnabled ?? false; + + public static bool DxvkHudEnabled => Program.Config.DxvkHud != DxvkHud.None; + + public static string DxvkHudString => Program.Config.DxvkHud switch + { + DxvkHud.None => "", + DxvkHud.Custom => Program.Config.DxvkHudCustom ?? "", + DxvkHud.Default => "1", + DxvkHud.Fps => "fps", + DxvkHud.Full => "full", + _ => throw new ArgumentOutOfRangeException(), + }; + + public static bool MangoHudInstalled { get; } + + public static bool MangoHudEnabled => Program.Config.MangoHud != MangoHud.None; + + public static bool MangoHudCustomIsFile => Program.Config.MangoHud == MangoHud.CustomFile; + + public static string MangoHudString => Program.Config.MangoHud switch + { + MangoHud.None => "", + MangoHud.Default => "", + MangoHud.Full => "full", + MangoHud.CustomString => Program.Config.MangoHudCustomString ?? "", + MangoHud.CustomFile => Program.Config.MangoHudCustomFile ?? "", + _ => throw new ArgumentOutOfRangeException(), + }; + + public static string DXVK_HUD => "fps,frametimes,gpuload,version"; + + public static string MANGOHUD_CONFIG => "ram,vram,resolution,vulkan_driver,engine_version,wine,frame_timing=0"; + + public static string MANGOHUD_CONFIGFILE => Path.Combine(CoreEnvironmentSettings.XDG_CONFIG_HOME, "MangoHud", "MangoHud.conf"); + + public static Dictionary> Versions { get; private set; } + + static Dxvk() + { + Versions = new Dictionary>(); + MangoHudInstalled = DxvkSettings.MangoHudIsInstalled(); + } + + public static void Initialize() + { + // Add default versions. + Versions["dxvk-2.4.1"] = new Dictionary() + { + {"name", "DXVK 2.4.1"}, {"desc", "Latest version, using Graphics Pipeline Libs. Async no longer needed."}, + {"label", "Current"}, {"url", "https://github.com/doitsujin/dxvk/releases/download/v2.4.1/dxvk-2.4.1.tar.gz"}, + {"mark", "Download" } + }; + Versions["dxvk-2.2"] = new Dictionary() + { + {"name", "DXVK 2.2"}, {"desc", "Previous version, using Graphics Pipeline Libs. Use this if you have problems with ReShade Effects Toggler (REST)."}, + {"label", "Previous"}, {"url", "https://github.com/doitsujin/dxvk/releases/download/v2.2/dxvk-2.2.tar.gz"}, + {"mark", "Download" } + }; + Versions["dxvk-async-1.10.3"] = new Dictionary() + { + {"name", "DXVK 1.10.3"}, {"desc", "Legacy version with high compatibility. Includes async patch."}, + {"label", "Legacy"}, {"url", "https://github.com/Sporif/dxvk-async/releases/download/1.10.3/dxvk-async-1.10.3.tar.gz"}, + {"mark", "Download" } + }; + Versions["DISABLED"] = new Dictionary() + { + {"name", "WineD3D"}, {"desc", "Use WineD3D (OpenGL) instead of DXVK. For old GPUs without Vulkan support."}, + {"label", "Disabled"} + }; + + var toolDirectory = new DirectoryInfo(Path.Combine(Program.storage.Root.FullName, "compatibilitytool", "dxvk")); + + if (!toolDirectory.Exists) + { + Program.storage.GetFolder("compatibilitytool/dxvk"); + return; + } + + foreach (var dxvkDir in toolDirectory.EnumerateDirectories().OrderBy(x => x.Name)) + { + if (Directory.Exists(Path.Combine(dxvkDir.FullName, "x64")) && Directory.Exists(Path.Combine(dxvkDir.FullName, "x32"))) + { + if (Versions.ContainsKey(dxvkDir.Name)) + { + if (dxvkDir.Name == "DISABLED") + Log.Error("Cannot use custom DXVK with folder name DISABLED. Skipping."); + else + Versions[dxvkDir.Name].Remove("mark"); + continue; + } + Versions[dxvkDir.Name] = new Dictionary() { {"label", "Custom"} }; + } + } + } + + private static string GetDownloadUrl(string? name) + { + name ??= GetDefaultVersion(); + if (Versions.ContainsKey(name)) + return Versions[name].ContainsKey("url") ? Versions[name]["url"] : ""; + return Versions[GetDefaultVersion()]["url"]; + } + + public static string GetDefaultVersion() + { + if (Versions.ContainsKey("dxvk-async-1.10.3")) + return "dxvk-async-1.10.3"; + if (Versions.ContainsKey("dxvk-2.4")) + return "dxvk-2.4"; + return Versions.First().Key; + } + +} + +public enum DxvkHud +{ + [SettingsDescription("None", "Disable DXVK Hud")] + None, + + [SettingsDescription("FPS", "Only show FPS")] + Fps, + + [SettingsDescription("Default", "Equivalent to DXVK_HUD=1")] + Default, + + [SettingsDescription("Custom", "Use a custom DXVK_HUD string")] + Custom, + + [SettingsDescription("Full", "Show everything")] + Full, +} + +public enum MangoHud +{ + [SettingsDescription("None", "Disable MangoHud")] + None, + + [SettingsDescription("Default", "Uses no config file.")] + Default, + + [SettingsDescription("Custom File", "Specify a custom config file")] + CustomFile, + + [SettingsDescription("Custom String", "Specify a config via string")] + CustomString, + + [SettingsDescription("Full", "Show (almost) everything")] + Full, +} + diff --git a/src/XIVLauncher.Core/UnixCompatibility/Wine.cs b/src/XIVLauncher.Core/UnixCompatibility/Wine.cs new file mode 100644 index 00000000..0e03ecf7 --- /dev/null +++ b/src/XIVLauncher.Core/UnixCompatibility/Wine.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Diagnostics; +using System.IO; +using System.Linq; +using Serilog; +using XIVLauncher.Common; +using XIVLauncher.Common.Unix; +using XIVLauncher.Common.Unix.Compatibility; + +namespace XIVLauncher.Core.UnixCompatibility; + +public static class Wine +{ + public static bool IsManagedWine => Program.Config.WineType == WineType.Managed; + + public static string CustomWinePath => Program.Config.WineBinaryPath ?? "/usr/bin"; + + public static string FolderName => Program.Config.WineVersion ?? GetDefaultVersion(); + + public static string DownloadUrl => GetDownloadUrl(Program.Config.WineVersion); + + public static string DebugVars => Program.Config.WineDebugVars ?? "-all"; + + public static FileInfo LogFile => new FileInfo(Path.Combine(Program.storage.GetFolder("logs").FullName, "wine.log")); + + public static DirectoryInfo Prefix => Program.storage.GetFolder("wineprefix"); + + public static bool ESyncEnabled => Program.Config.ESyncEnabled ?? true; + + public static bool FSyncEnabled => Program.Config.FSyncEnabled ?? false; + + public static Dictionary> Versions { get; private set; } + + static Wine() + { + Versions = new Dictionary>(); + } + + public static void Initialize() + { + // Add default versions. + Versions["wine-xiv-staging-fsync-git-7.10.r3.g560db77d"] = new Dictionary() + { + {"name", "Wine-XIV 7.10"}, {"desc","Patched version of Wine Staging 7.10. Default."}, + {"label", "Official"}, {"url", $"https://github.com/goatcorp/wine-xiv-git/releases/download/7.10.r3.g560db77d/wine-xiv-staging-fsync-git-{LinuxInfo.Package.ToString()}-7.10.r3.g560db77d.tar.xz"}, + {"mark", "Download"} + }; + Versions["wine-xiv-staging-fsync-git-8.5.r4.g4211bac7"] = new Dictionary() + { + {"name", "Wine-XIV 8.5"}, {"desc", "Patched version of Wine Staging 8.5. Change Windows version to 7 for best results."}, + {"label", "Official"}, {"url", $"https://github.com/goatcorp/wine-xiv-git/releases/download/8.5.r4.g4211bac7/wine-xiv-staging-fsync-git-{LinuxInfo.Package.ToString()}-8.5.r4.g4211bac7.tar.xz"}, + {"mark", "Download"} + }; + + var toolDirectory = new DirectoryInfo(Path.Combine(Program.storage.Root.FullName, "compatibilitytool", "wine")); + + if (!toolDirectory.Exists) + { + Program.storage.GetFolder("compatibilitytool/wine"); + return; + } + + foreach (var wineDir in toolDirectory.EnumerateDirectories().OrderBy(x => x.Name)) + { + if (File.Exists(Path.Combine(wineDir.FullName, "bin", "wine64")) || + File.Exists(Path.Combine(wineDir.FullName, "bin", "wine"))) + { + if (Versions.ContainsKey(wineDir.Name)) + { + Versions[wineDir.Name].Remove("mark"); + continue; + } + Versions[wineDir.Name] = new Dictionary() { {"label", "Custom"} }; + } + } + } + + private static string GetDownloadUrl(string? name) + { + name ??= GetDefaultVersion(); + if (Versions.ContainsKey(name)) + return Versions[name].ContainsKey("url") ? Versions[name]["url"] : ""; + return Versions[GetDefaultVersion()]["url"]; + } + + public static string GetDefaultVersion() + { + if (Versions.ContainsKey("wine-xiv-staging-fsync-git-8.5.r4.g4211bac7")) + return "wine-xiv-staging-fsync-git-8.5.r4.g4211bac7"; + if (Versions.ContainsKey("wine-xiv-staging-fsync-git-7.10.r3.g560db77d")) + return "wine-xiv-staging-fsync-git-7.10.r3.g560db77d"; + return Versions.First().Key; + } +} + +public enum WineType +{ + [SettingsDescription("Managed by XIVLauncher", "Choose a patched version of wine made specifically for XIVLauncher")] + Managed, + + [SettingsDescription("Custom", "Point XIVLauncher to a custom location containing wine binaries to run the game with.")] + Custom, +} \ No newline at end of file