diff --git a/lib/FFXIVQuickLauncher b/lib/FFXIVQuickLauncher index cdde1fbe..19c603de 160000 --- a/lib/FFXIVQuickLauncher +++ b/lib/FFXIVQuickLauncher @@ -1 +1 @@ -Subproject commit cdde1fbeb31549dea13bd015e76b0b3964543c12 +Subproject commit 19c603de1ec038136bdb14d65924bd525131d3fb diff --git a/src/XIVLauncher.Common.Unix/Compatibility/CompatibilityTools.cs b/src/XIVLauncher.Common.Unix/Compatibility/CompatibilityTools.cs new file mode 100644 index 00000000..8216526f --- /dev/null +++ b/src/XIVLauncher.Common.Unix/Compatibility/CompatibilityTools.cs @@ -0,0 +1,293 @@ +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 XIVLauncher.Common.Util; + +#if FLATPAK +#warning THIS IS A FLATPAK BUILD!!! +#endif + +namespace XIVLauncher.Common.Unix.Compatibility; + +public class CompatibilityTools +{ + private DirectoryInfo toolDirectory; + 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; + + private readonly Dxvk.DxvkHudType hudType; + private readonly bool gamemodeOn; + private readonly string dxvkAsyncOn; + + public CompatibilityTools(WineSettings wineSettings, Dxvk.DxvkHudType hudType, bool? gamemodeOn, bool? dxvkAsyncOn, DirectoryInfo toolsFolder) + { + this.Settings = wineSettings; + this.hudType = hudType; + this.gamemodeOn = gamemodeOn ?? false; + this.dxvkAsyncOn = (dxvkAsyncOn ?? false) ? "1" : "0"; + + this.toolDirectory = new DirectoryInfo(Path.Combine(toolsFolder.FullName, "beta")); + 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.dxvkDirectory.Exists) + this.dxvkDirectory.Create(); + } + + if (!wineSettings.Prefix.Exists) + wineSettings.Prefix.Create(); + } + + public async Task EnsureTool(DirectoryInfo tempPath) + { + if (!File.Exists(Wine64Path)) + { + Log.Information("Compatibility tool does not exist, downloading"); + await DownloadTool(tempPath).ConfigureAwait(false); + } + + EnsurePrefix(); + await Dxvk.InstallDxvk(Settings.Prefix, dxvkDirectory).ConfigureAwait(false); + + IsToolReady = true; + } + + private async Task DownloadTool(DirectoryInfo tempPath) + { + using var client = new HttpClient(); + var tempFilePath = Path.Combine(tempPath.FullName, $"{Guid.NewGuid()}"); + + await File.WriteAllBytesAsync(tempFilePath, await client.GetByteArrayAsync(WINE_XIV_RELEASE_URL).ConfigureAwait(false)).ConfigureAwait(false); + + PlatformHelpers.Untar(tempFilePath, this.toolDirectory.FullName); + + Log.Information("Compatibility tool successfully extracted to {Path}", this.toolDirectory.FullName); + + File.Delete(tempFilePath); + } + + private void ResetPrefix() + { + Settings.Prefix.Refresh(); + + if (Settings.Prefix.Exists) + Settings.Prefix.Delete(true); + + Settings.Prefix.Create(); + EnsurePrefix(); + } + + public void EnsurePrefix() + { + RunInPrefix("cmd /c dir %userprofile%/Documents > nul").WaitForExit(); + } + + public Process RunInPrefix(string command, string workingDirectory = "", IDictionary environment = null, bool redirectOutput = false, bool writeLog = false, bool wineD3D = false) + { + var psi = new ProcessStartInfo(Wine64Path); + psi.Arguments = command; + + Log.Verbose("Running in prefix: {FileName} {Arguments}", psi.FileName, command); + return RunInPrefix(psi, workingDirectory, environment, redirectOutput, writeLog, wineD3D); + } + + public Process RunInPrefix(string[] args, string workingDirectory = "", IDictionary environment = null, bool redirectOutput = false, bool writeLog = false, bool wineD3D = false) + { + var psi = new ProcessStartInfo(Wine64Path); + foreach (var arg in args) + psi.ArgumentList.Add(arg); + + Log.Verbose("Running in prefix: {FileName} {Arguments}", psi.FileName, psi.ArgumentList.Aggregate(string.Empty, (a, b) => a + " " + b)); + return RunInPrefix(psi, workingDirectory, environment, redirectOutput, writeLog, wineD3D); + } + + private void MergeDictionaries(StringDictionary a, IDictionary b) + { + if (b is null) + return; + + foreach (var keyValuePair in b) + { + if (a.ContainsKey(keyValuePair.Key)) + a[keyValuePair.Key] = keyValuePair.Value; + else + a.Add(keyValuePair.Key, keyValuePair.Value); + } + } + + private Process RunInPrefix(ProcessStartInfo psi, string workingDirectory, IDictionary environment, bool redirectOutput, bool writeLog, bool wineD3D) + { + psi.RedirectStandardOutput = redirectOutput; + psi.RedirectStandardError = writeLog; + 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")}"); + + if (!string.IsNullOrEmpty(Settings.DebugVars)) + { + wineEnviromentVariables.Add("WINEDEBUG", Settings.DebugVars); + } + + wineEnviromentVariables.Add("XL_WINEONLINUX", "true"); + string ldPreload = Environment.GetEnvironmentVariable("LD_PRELOAD") ?? ""; + + 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); + + wineEnviromentVariables.Add("LD_PRELOAD", ldPreload); + + MergeDictionaries(psi.EnvironmentVariables, wineEnviromentVariables); + MergeDictionaries(psi.EnvironmentVariables, environment); + +#if FLATPAK_NOTRIGHTNOW + psi.FileName = "flatpak-spawn"; + + psi.ArgumentList.Insert(0, "--host"); + psi.ArgumentList.Insert(1, Wine64Path); + + foreach (KeyValuePair envVar in wineEnviromentVariables) + { + psi.ArgumentList.Insert(1, $"--env={envVar.Key}={envVar.Value}"); + } + + if (environment != null) + { + foreach (KeyValuePair envVar in environment) + { + psi.ArgumentList.Insert(1, $"--env=\"{envVar.Key}\"=\"{envVar.Value}\""); + } + } +#endif + + Process helperProcess = new(); + helperProcess.StartInfo = psi; + helperProcess.ErrorDataReceived += new DataReceivedEventHandler((_, errLine) => + { + if (String.IsNullOrEmpty(errLine.Data)) + return; + + try + { + logWriter.WriteLine(errLine.Data); + Console.Error.WriteLine(errLine.Data); + } + catch (Exception ex) when (ex is ArgumentOutOfRangeException || + ex is OverflowException || + ex is IndexOutOfRangeException) + { + // very long wine log lines get chopped off after a (seemingly) arbitrary limit resulting in strings that are not null terminated + //logWriter.WriteLine("Error writing Wine log line:"); + //logWriter.WriteLine(ex.Message); + } + }); + + helperProcess.Start(); + if (writeLog) + helperProcess.BeginErrorReadLine(); + + return helperProcess; + } + + public Int32[] GetProcessIds(string executableName) + { + var wineDbg = RunInPrefix("winedbg --command \"info proc\"", redirectOutput: true); + var output = wineDbg.StandardOutput.ReadToEnd(); + var matchingLines = output.Split('\n', StringSplitOptions.RemoveEmptyEntries).Where(l => l.Contains(executableName)); + return matchingLines.Select(l => int.Parse(l.Substring(1, 8), System.Globalization.NumberStyles.HexNumber)).ToArray(); + } + + public Int32 GetProcessId(string executableName) + { + return GetProcessIds(executableName).FirstOrDefault(); + } + + 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; + 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(); + } + + public string UnixToWinePath(string unixPath) + { + var launchArguments = new string[] { "winepath", "--windows", unixPath }; + var winePath = RunInPrefix(launchArguments, redirectOutput: true); + var output = winePath.StandardOutput.ReadToEnd(); + return output.Split('\n', StringSplitOptions.RemoveEmptyEntries).LastOrDefault(); + } + + public void AddRegistryKey(string key, string value, string data) + { + var args = new string[] { "reg", "add", key, "/v", value, "/d", data, "/f" }; + var wineProcess = RunInPrefix(args); + wineProcess.WaitForExit(); + } + + public void Kill() + { + var psi = new ProcessStartInfo(WineServerPath) + { + Arguments = "-k" + }; + psi.EnvironmentVariables.Add("WINEPREFIX", Settings.Prefix.FullName); + + Process.Start(psi); + } +} \ No newline at end of file diff --git a/src/XIVLauncher.Common.Unix/Compatibility/Dxvk.cs b/src/XIVLauncher.Common.Unix/Compatibility/Dxvk.cs new file mode 100644 index 00000000..62e3a303 --- /dev/null +++ b/src/XIVLauncher.Common.Unix/Compatibility/Dxvk.cs @@ -0,0 +1,55 @@ +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/GameFixes/GameFix.cs b/src/XIVLauncher.Common.Unix/Compatibility/GameFixes/GameFix.cs new file mode 100644 index 00000000..561107d1 --- /dev/null +++ b/src/XIVLauncher.Common.Unix/Compatibility/GameFixes/GameFix.cs @@ -0,0 +1,28 @@ +using System.IO; + +namespace XIVLauncher.Common.Unix.Compatibility.GameFixes; + +public abstract class GameFix +{ + public GameFix(DirectoryInfo gameDirectory, DirectoryInfo configDirectory, DirectoryInfo winePrefixDirectory, DirectoryInfo tempDirectory) + { + GameDir = gameDirectory; + ConfigDir = configDirectory; + WinePrefixDir = winePrefixDirectory; + TempDir = tempDirectory; + } + + public abstract string LoadingTitle { get; } + + public GameFixApply.UpdateProgressDelegate UpdateProgress; + + public DirectoryInfo WinePrefixDir { get; private set; } + + public DirectoryInfo ConfigDir { get; private set; } + + public DirectoryInfo GameDir { get; private set; } + + public DirectoryInfo TempDir { get; private set; } + + public abstract void Apply(); +} \ No newline at end of file diff --git a/src/XIVLauncher.Common.Unix/Compatibility/GameFixes/GameFixApply.cs b/src/XIVLauncher.Common.Unix/Compatibility/GameFixes/GameFixApply.cs new file mode 100644 index 00000000..4c11d402 --- /dev/null +++ b/src/XIVLauncher.Common.Unix/Compatibility/GameFixes/GameFixApply.cs @@ -0,0 +1,29 @@ +using System.IO; + +namespace XIVLauncher.Common.Unix.Compatibility.GameFixes; + +public class GameFixApply +{ + private readonly GameFix[] fixes; + + public delegate void UpdateProgressDelegate(string loadingText, bool hasProgress, float progress); + + public event UpdateProgressDelegate UpdateProgress; + + public GameFixApply(DirectoryInfo gameDirectory, DirectoryInfo configDirectory, DirectoryInfo winePrefixDirectory, DirectoryInfo tempDirectory) + { + this.fixes = new GameFix[] { }; + } + + public void Run() + { + foreach (GameFix fix in this.fixes) + { + this.UpdateProgress?.Invoke(fix.LoadingTitle, false, 0f); + + fix.UpdateProgress += this.UpdateProgress; + fix.Apply(); + fix.UpdateProgress -= this.UpdateProgress; + } + } +} \ No newline at end of file diff --git a/src/XIVLauncher.Common.Unix/Compatibility/WineSettings.cs b/src/XIVLauncher.Common.Unix/Compatibility/WineSettings.cs new file mode 100644 index 00000000..9b122f7a --- /dev/null +++ b/src/XIVLauncher.Common.Unix/Compatibility/WineSettings.cs @@ -0,0 +1,37 @@ +using System.IO; + +namespace XIVLauncher.Common.Unix.Compatibility; + +public enum WineStartupType +{ + [SettingsDescription("Managed by XIVLauncher", "The game installation and wine setup is managed by XIVLauncher - you can leave it up to us.")] + Managed, + + [SettingsDescription("Custom", "Point XIVLauncher to a custom location containing wine binaries to run the game with.")] + Custom, +} + +public class WineSettings +{ + public WineStartupType StartupType { get; private set; } + public string CustomBinPath { get; private set; } + + 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) + { + this.StartupType = startupType ?? WineStartupType.Custom; + this.CustomBinPath = customBinPath; + this.EsyncOn = (esyncOn ?? false) ? "1" : "0"; + this.FsyncOn = (fsyncOn ?? false) ? "1" : "0"; + this.DebugVars = debugVars; + this.LogFile = logFile; + this.Prefix = prefix; + } +} \ No newline at end of file diff --git a/src/XIVLauncher.Common.Unix/UnixDalamudCompatibilityCheck.cs b/src/XIVLauncher.Common.Unix/UnixDalamudCompatibilityCheck.cs new file mode 100644 index 00000000..081d2a54 --- /dev/null +++ b/src/XIVLauncher.Common.Unix/UnixDalamudCompatibilityCheck.cs @@ -0,0 +1,33 @@ +using System.Runtime.InteropServices; +using XIVLauncher.Common.PlatformAbstractions; + +namespace XIVLauncher.Common.Unix; + +public class UnixDalamudCompatibilityCheck : IDalamudCompatibilityCheck +{ + public void EnsureCompatibility() + { + //Dalamud will work with wines built-in vcrun, so no need to check that + EnsureArchitecture(); + } + + private static void EnsureArchitecture() + { + var arch = RuntimeInformation.ProcessArchitecture; + + switch (arch) + { + case Architecture.X86: + throw new IDalamudCompatibilityCheck.ArchitectureNotSupportedException("Dalamud is not supported on x86 architecture."); + + case Architecture.X64: + break; + + case Architecture.Arm: + throw new IDalamudCompatibilityCheck.ArchitectureNotSupportedException("Dalamud is not supported on ARM32."); + + case Architecture.Arm64: + throw new IDalamudCompatibilityCheck.ArchitectureNotSupportedException("x64 emulation was not detected. Please make sure to run XIVLauncher with x64 emulation."); + } + } +} \ No newline at end of file diff --git a/src/XIVLauncher.Common.Unix/UnixDalamudRunner.cs b/src/XIVLauncher.Common.Unix/UnixDalamudRunner.cs new file mode 100644 index 00000000..ef1b8545 --- /dev/null +++ b/src/XIVLauncher.Common.Unix/UnixDalamudRunner.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Serilog; +using XIVLauncher.Common.Dalamud; +using XIVLauncher.Common.PlatformAbstractions; +using XIVLauncher.Common.Unix.Compatibility; + +namespace XIVLauncher.Common.Unix; + +public class UnixDalamudRunner : IDalamudRunner +{ + private readonly CompatibilityTools compatibility; + private readonly DirectoryInfo dotnetRuntime; + + public UnixDalamudRunner(CompatibilityTools compatibility, DirectoryInfo dotnetRuntime) + { + this.compatibility = compatibility; + this.dotnetRuntime = dotnetRuntime; + } + + public Process? Run(FileInfo runner, bool fakeLogin, bool noPlugins, bool noThirdPlugins, FileInfo gameExe, string gameArgs, IDictionary environment, DalamudLoadMethod loadMethod, DalamudStartInfo startInfo) + { + var gameExePath = ""; + var dotnetRuntimePath = ""; + + Parallel.Invoke( + () => { gameExePath = compatibility.UnixToWinePath(gameExe.FullName); }, + () => { dotnetRuntimePath = compatibility.UnixToWinePath(dotnetRuntime.FullName); }, + () => { startInfo.LoggingPath = compatibility.UnixToWinePath(startInfo.LoggingPath); }, + () => { startInfo.WorkingDirectory = compatibility.UnixToWinePath(startInfo.WorkingDirectory); }, + () => { startInfo.ConfigurationPath = compatibility.UnixToWinePath(startInfo.ConfigurationPath); }, + () => { startInfo.PluginDirectory = compatibility.UnixToWinePath(startInfo.PluginDirectory); }, + () => { startInfo.AssetDirectory = compatibility.UnixToWinePath(startInfo.AssetDirectory); } + ); + + var prevDalamudRuntime = Environment.GetEnvironmentVariable("DALAMUD_RUNTIME"); + if (string.IsNullOrWhiteSpace(prevDalamudRuntime)) + environment.Add("DALAMUD_RUNTIME", dotnetRuntimePath); + + var launchArguments = new List + { + $"\"{runner.FullName}\"", + DalamudInjectorArgs.LAUNCH, + DalamudInjectorArgs.Mode(loadMethod == DalamudLoadMethod.EntryPoint ? "entrypoint" : "inject"), + DalamudInjectorArgs.Game(gameExePath), + DalamudInjectorArgs.WorkingDirectory(startInfo.WorkingDirectory), + DalamudInjectorArgs.ConfigurationPath(startInfo.ConfigurationPath), + DalamudInjectorArgs.LoggingPath(startInfo.LoggingPath), + DalamudInjectorArgs.PluginDirectory(startInfo.PluginDirectory), + DalamudInjectorArgs.AssetDirectory(startInfo.AssetDirectory), + DalamudInjectorArgs.ClientLanguage((int)startInfo.Language), + DalamudInjectorArgs.DelayInitialize(startInfo.DelayInitializeMs), + DalamudInjectorArgs.TsPackB64(Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(startInfo.TroubleshootingPackData))), + }; + + if (loadMethod == DalamudLoadMethod.ACLonly) + launchArguments.Add(DalamudInjectorArgs.WITHOUT_DALAMUD); + + if (fakeLogin) + launchArguments.Add(DalamudInjectorArgs.FAKE_ARGUMENTS); + + if (noPlugins) + launchArguments.Add(DalamudInjectorArgs.NO_PLUGIN); + + if (noThirdPlugins) + launchArguments.Add(DalamudInjectorArgs.NO_THIRD_PARTY); + + launchArguments.Add("--"); + 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"); + + Console.WriteLine(output); + + new Thread(() => + { + while (!dalamudProcess.StandardOutput.EndOfStream) + { + var output = dalamudProcess.StandardOutput.ReadLine(); + 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"); + return null; + } + + var gameProcess = Process.GetProcessById(unixPid); + Log.Verbose($"Got game process handle {gameProcess.Handle} with Unix pid {gameProcess.Id} and Wine pid {dalamudConsoleOutput.Pid}"); + return gameProcess; + } + catch (JsonReaderException ex) + { + Log.Error(ex, $"Couldn't parse Dalamud output: {output}"); + return null; + } + } +} \ No newline at end of file diff --git a/src/XIVLauncher.Common.Unix/UnixGameRunner.cs b/src/XIVLauncher.Common.Unix/UnixGameRunner.cs new file mode 100644 index 00000000..e612377d --- /dev/null +++ b/src/XIVLauncher.Common.Unix/UnixGameRunner.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using XIVLauncher.Common.Dalamud; +using XIVLauncher.Common.PlatformAbstractions; +using XIVLauncher.Common.Unix.Compatibility; + +namespace XIVLauncher.Common.Unix; + +public class UnixGameRunner : IGameRunner +{ + private readonly CompatibilityTools compatibility; + private readonly DalamudLauncher dalamudLauncher; + private readonly bool dalamudOk; + + public UnixGameRunner(CompatibilityTools compatibility, DalamudLauncher dalamudLauncher, bool dalamudOk) + { + this.compatibility = compatibility; + this.dalamudLauncher = dalamudLauncher; + this.dalamudOk = dalamudOk; + } + + public Process? Start(string path, string workingDirectory, string arguments, IDictionary environment, DpiAwareness dpiAwareness) + { + if (dalamudOk) + { + return this.dalamudLauncher.Run(new FileInfo(path), arguments, environment); + } + else + { + return compatibility.RunInPrefix($"\"{path}\" {arguments}", workingDirectory, environment, writeLog: true); + } + } +} \ No newline at end of file diff --git a/src/XIVLauncher.Common.Unix/UnixSteam.cs b/src/XIVLauncher.Common.Unix/UnixSteam.cs new file mode 100644 index 00000000..fb1aed1f --- /dev/null +++ b/src/XIVLauncher.Common.Unix/UnixSteam.cs @@ -0,0 +1,81 @@ +using System; +using System.Threading.Tasks; +using Steamworks; +using XIVLauncher.Common.PlatformAbstractions; + +namespace XIVLauncher.Common.Unix +{ + public class UnixSteam : ISteam + { + public UnixSteam() + { + SteamUtils.OnGamepadTextInputDismissed += b => OnGamepadTextInputDismissed?.Invoke(b); + } + + public void Initialize(uint appId) + { + // workaround because SetEnvironmentVariable doesn't actually touch the process environment on unix + [System.Runtime.InteropServices.DllImport("c")] + static extern int setenv(string name, string value, int overwrite); + + setenv("SteamAppId", appId.ToString(), 1); + setenv("SteamGameId", appId.ToString(), 1); + + SteamClient.Init(appId); + } + + public bool IsValid => SteamClient.IsValid; + + public bool BLoggedOn => SteamClient.IsLoggedOn; + + public bool BOverlayNeedsPresent => SteamUtils.DoesOverlayNeedPresent; + + public void Shutdown() + { + SteamClient.Shutdown(); + } + + public async Task GetAuthSessionTicketAsync() + { + var ticket = await SteamUser.GetAuthSessionTicketAsync().ConfigureAwait(true); + return ticket?.Data; + } + + public bool IsAppInstalled(uint appId) + { + return SteamApps.IsAppInstalled(appId); + } + + public string GetAppInstallDir(uint appId) + { + return SteamApps.AppInstallDir(appId); + } + + public bool ShowGamepadTextInput(bool password, bool multiline, string description, int maxChars, string existingText = "") + { + return SteamUtils.ShowGamepadTextInput(password ? GamepadTextInputMode.Password : GamepadTextInputMode.Normal, multiline ? GamepadTextInputLineMode.MultipleLines : GamepadTextInputLineMode.SingleLine, description, maxChars, existingText); + } + + public string GetEnteredGamepadText() + { + return SteamUtils.GetEnteredGamepadText(); + } + + public bool ShowFloatingGamepadTextInput(ISteam.EFloatingGamepadTextInputMode mode, int x, int y, int width, int height) + { + SteamUtils.ShowFloatingGamepadTextInput((TextInputMode)mode, x, y, width, height); + return true; + } + + public bool IsRunningOnSteamDeck() => SteamUtils.IsRunningOnSteamDeck; + + public uint GetServerRealTime() => (uint)((DateTimeOffset)SteamUtils.SteamServerTime).ToUnixTimeSeconds(); + + public void ActivateGameOverlayToWebPage(string url, bool modal = false) + { + SteamFriends.OpenWebOverlay(url, modal); + } + + public event Action OnGamepadTextInputDismissed; + } +} diff --git a/src/XIVLauncher.Common.Unix/XIVLauncher.Common.Unix.csproj b/src/XIVLauncher.Common.Unix/XIVLauncher.Common.Unix.csproj new file mode 100644 index 00000000..6d01cdbe --- /dev/null +++ b/src/XIVLauncher.Common.Unix/XIVLauncher.Common.Unix.csproj @@ -0,0 +1,49 @@ + + + XIVLauncher.Common.Unix + XIVLauncher.Common.Unix + Shared XIVLauncher platform-specific implementations for Unix-like systems. + 1.0.0 + disable + + + + Library + net8.0 + latest + true + true + + + + $(DefineConstants);$(ExtraDefineConstants) + + + + + + + + $(MSBuildProjectDirectory)\ + $(AppOutputBase)=C:\goatsoft\xl\XIVLauncher.Common.Unix\ + + + + + + PreserveNewest + + + + + + + PreserveNewest + + + + + + + + \ No newline at end of file diff --git a/src/XIVLauncher.Common.Unix/libsteam_api64.dylib b/src/XIVLauncher.Common.Unix/libsteam_api64.dylib new file mode 100644 index 00000000..8d3c5eeb Binary files /dev/null and b/src/XIVLauncher.Common.Unix/libsteam_api64.dylib differ diff --git a/src/XIVLauncher.Common.Unix/libsteam_api64.so b/src/XIVLauncher.Common.Unix/libsteam_api64.so new file mode 100644 index 00000000..8bf6762b Binary files /dev/null and b/src/XIVLauncher.Common.Unix/libsteam_api64.so differ diff --git a/src/XIVLauncher.Core/XIVLauncher.Core.csproj b/src/XIVLauncher.Core/XIVLauncher.Core.csproj index 23970b63..ccb30da0 100644 --- a/src/XIVLauncher.Core/XIVLauncher.Core.csproj +++ b/src/XIVLauncher.Core/XIVLauncher.Core.csproj @@ -52,7 +52,7 @@ + Include="..\XIVLauncher.Common.Unix\XIVLauncher.Common.Unix.csproj" />